SegmentFault 河岸最新的文章
2019-06-27T15:34:13+08:00
https://segmentfault.com/feeds/blogs
https://creativecommons.org/licenses/by-nc-nd/4.0/
管理系统之权限的设计和实现
https://segmentfault.com/a/1190000019599724
2019-06-27T15:34:13+08:00
2019-06-27T15:34:13+08:00
我在长安长安
https://segmentfault.com/u/wozaichanganchangan
36
<p><strong>本文主要想对前端权限管理功能实现做一个分享,所以并不会对后台管理的框架结构做太详细介绍,如果有朋友对其他有兴趣可以留言。</strong></p>
<h2>基本设计和分析</h2>
<ul>
<li>前端 vue + elementui</li>
<li>服务端: node + mysql + nginx</li>
</ul>
<h3>主要功能</h3>
<p>打开思否页面,根据页面的功能点,设计出相关的数据表,和管理系统需要的相关页面。<br>计划后台管理需要完成的功能:</p>
<ul>
<li>权限管理(菜单权限到数据权限) -- 已完成</li>
<li>工作流 (问答和文章在某个条件内,提交需要走流程)-- 未完成</li>
<li>socket (对用户点赞,评论,系统通知等消息进行实时推送)-- 未完成</li>
<li>文件管理(将页面需要用到的文件上传管理,其他页面都统一访问文件库资源)-- 已完成</li>
<li>基本业务 (业务页面)-- 部分完成</li>
</ul>
<h3>模块相关介绍</h3>
<table>
<thead><tr>
<th align="left">模块</th>
<th align="left">功能</th>
<th align="left">页面编码</th>
<th align="left">描述</th>
</tr></thead>
<tbody>
<tr>
<td align="left">登录</td>
<td align="left">登录</td>
<td align="left">login</td>
<td align="left">菜单中不显示</td>
</tr>
<tr>
<td align="left">401</td>
<td align="left">401</td>
<td align="left">401</td>
<td align="left">角色无访问权限时进入这个页面</td>
</tr>
<tr>
<td align="left">404</td>
<td align="left">404</td>
<td align="left">404</td>
<td align="left">访问菜单不存在时进入这个页面</td>
</tr>
<tr>
<td align="left">首页</td>
<td align="left">首页</td>
<td align="left">home</td>
<td align="left"> </td>
</tr>
<tr>
<td align="left">运维中心</td>
<td align="left"> </td>
<td align="left">opsCenter</td>
<td align="left"> </td>
</tr>
<tr>
<td align="left">-</td>
<td align="left">问答管理</td>
<td align="left">questionMan</td>
<td align="left"> </td>
</tr>
<tr>
<td align="left">-</td>
<td align="left">专栏管理</td>
<td align="left">blogMan</td>
<td align="left"> </td>
</tr>
<tr>
<td align="left">-</td>
<td align="left">文章管理</td>
<td align="left">articleMan</td>
<td align="left"> </td>
</tr>
<tr>
<td align="left">-</td>
<td align="left">讲堂管理</td>
<td align="left">liveMan</td>
<td align="left"> </td>
</tr>
<tr>
<td align="left">-</td>
<td align="left">活动管理</td>
<td align="left">activityMan</td>
<td align="left"> </td>
</tr>
<tr>
<td align="left">-</td>
<td align="left">广告位</td>
<td align="left">advertising</td>
<td align="left"> </td>
</tr>
<tr>
<td align="left">工作流</td>
<td align="left"> </td>
<td align="left">workflow</td>
<td align="left"> </td>
</tr>
<tr>
<td align="left">-</td>
<td align="left">流程设计</td>
<td align="left">processDesign</td>
<td align="left"> </td>
</tr>
<tr>
<td align="left">-</td>
<td align="left">业务管理</td>
<td align="left">businessMan</td>
<td align="left"> </td>
</tr>
<tr>
<td align="left">-</td>
<td align="left">已办事项</td>
<td align="left">finishedItems</td>
<td align="left"> </td>
</tr>
<tr>
<td align="left">-</td>
<td align="left">未办事项</td>
<td align="left">unfinishedItems</td>
<td align="left"> </td>
</tr>
<tr>
<td align="left">文件库</td>
<td align="left"> </td>
<td align="left">library</td>
<td align="left"> </td>
</tr>
<tr>
<td align="left">-</td>
<td align="left">图片管理</td>
<td align="left">imgMan</td>
<td align="left"> </td>
</tr>
<tr>
<td align="left">-</td>
<td align="left">文件管理</td>
<td align="left">fileMan</td>
<td align="left"> </td>
</tr>
<tr>
<td align="left">论坛配置</td>
<td align="left"> </td>
<td align="left">bbsConfig</td>
<td align="left"> </td>
</tr>
<tr>
<td align="left">-</td>
<td align="left">轮播</td>
<td align="left">carousel</td>
<td align="left"> </td>
</tr>
<tr>
<td align="left">-</td>
<td align="left">技术频道</td>
<td align="left">techSquare</td>
<td align="left"> </td>
</tr>
<tr>
<td align="left">-</td>
<td align="left">通知</td>
<td align="left">notices</td>
<td align="left"> </td>
</tr>
<tr>
<td align="left">-</td>
<td align="left">标签类型管理</td>
<td align="left">tagTypeMan</td>
<td align="left"> </td>
</tr>
<tr>
<td align="left">-</td>
<td align="left">标签管理</td>
<td align="left">tagMan</td>
<td align="left"> </td>
</tr>
<tr>
<td align="left">系统管理</td>
<td align="left"> </td>
<td align="left">sysMan</td>
<td align="left"> </td>
</tr>
<tr>
<td align="left">-</td>
<td align="left">用户管理</td>
<td align="left">userMan</td>
<td align="left"> </td>
</tr>
<tr>
<td align="left">-</td>
<td align="left">角色管理</td>
<td align="left">roleMan</td>
<td align="left"> </td>
</tr>
<tr>
<td align="left">-</td>
<td align="left">菜单管理</td>
<td align="left">menuMan</td>
<td align="left"> </td>
</tr>
<tr>
<td align="left">-</td>
<td align="left">区域管理</td>
<td align="left">areaMan</td>
<td align="left"> </td>
</tr>
<tr>
<td align="left">-</td>
<td align="left">图表配置</td>
<td align="left">chartConfig</td>
<td align="left"> </td>
</tr>
<tr>
<td align="left">-</td>
<td align="left">系统日志</td>
<td align="left">log</td>
<td align="left"> </td>
</tr>
</tbody>
</table>
<h3>代码结构</h3>
<pre><code>├── admin // 打包产出文件
├── node_module // npm加载所需的项目依赖模块
├── public // 静态入口
├── src // 源代码
│ ├── api // 所有请求
│ ├── assets // 主题 字体 图片等静态资源
│ ├── common // 全局公用配置
│ │ ├── config // 配置全局路由权限和错误捕获
│ │ ├── mixin // 一些vue公用的mixin
│ │ ├── js // 编写公有的方法
│ │ └── style // 编写公有的样式
│ ├── components // 全局公用组件
│ ├── directive // 自定义指令
│ ├── router // 路由
│ ├── store // 全局 store管理
│ ├── views // view
│ ├── App.vue // 入口页面
│ └── main.js // 入口 加载组件 初始化等
├── static // 第三方不打包资源
├── .babelrc // babel-loader 配置
├── eslintrc.js // eslint 配置项
├── .gitignore // git 忽略项
├── vue.config.js // vue-cli@3.0+ 配置文件
└── package.json // package.json</code></pre>
<h2>权限设计</h2>
<p>进入正文,关于权限设计,围绕的是前端页面,但是会将前端和后端的逻辑都讲出来。</p>
<h3>用户管理</h3>
<h4>创建</h4>
<p><img src="/img/bVbuou0?w=1922&h=544" alt="clipboard.png" title="clipboard.png"></p>
<h5>前端页面</h5>
<p>看图中圈起来的地方,前端看到的逻辑是这样的:</p>
<ul>
<li>当前用户为<strong>admin</strong>
</li>
<li>
<strong>树用右键操作</strong>是<strong>admin</strong>创建的用户</li>
<li>
<strong>树用右键操作</strong>创建的用户<strong>admin</strong>可以管理</li>
</ul>
<p>就是创建了一个用户,这个用户创建的用户以及创建用户创建的用户,都可以被当前创建者管理。</p>
<h5>接口逻辑</h5>
<ul>
<li>查询到数据库中所有的用户ID</li>
<li>通过用户ID和创建人ID的关系,通过建立树状数据,得到当前用户创建的用户树</li>
<li>递归从用户树中得到所有属于当前用户子集的用户ID</li>
<li>select * from table where id in (子集用户id)</li>
</ul>
<p>通过这个逻辑,可以得到所有当前用户创建的子集,但是第一步有很大的问题,一旦用户数量巨大,这样查询会很慢。母目前只是为了功能实现,暂未考虑到性能方面,如果有好的方法,希望指点。</p>
<h4>删除</h4>
<p><img src="/img/bVbuoza?w=1191&h=661" alt="clipboard.png" title="clipboard.png"></p>
<p><img src="/img/bVbuozk?w=1200&h=665" alt="clipboard.png" title="clipboard.png"></p>
<h5>前端页面</h5>
<ol>
<li>删除用户,调用接口判断用户是否有子集,存在->3,不存在->2</li>
<li>不存在直接删除</li>
<li>存在需要先将当前创建的用户转移给其他用户(其他用户不可为他的子集)</li>
<li>将用户转移成功,则此时子集为空 ->2</li>
</ol>
<h5>接口逻辑</h5>
<ol>
<li>查询到数据库中是否存在创建人ID为当前要删除的用户ID</li>
<li>存在则无法删除当前用户</li>
<li>前端调用户转移接口,将当前用户创建的用户转移给其他人后,此时可删除该用户</li>
</ol>
<h3>菜单管理</h3>
<p>菜单设计的时候分为三个类型,管理平台,论坛,移动端,但是不一定会写完,感觉一个人写好累呀~~~~<br>通过菜单又分还有默认布局组件和页面组件的区分,布局组件为layout,页面组件则为他的子路由,通过嵌套的形式,组成一个完整的页面。<br><img src="/img/bVbuoDp?w=245&h=566" alt="clipboard.png" title="clipboard.png"></p>
<h4>页面</h4>
<p><img src="/img/bVbuoDL?w=1004&h=624" alt="clipboard.png" title="clipboard.png"><br>目前页面上都是通过右键点击树组件,进入操作,如图所示,可以对菜单进行增删改查操作。</p>
<blockquote>菜单字段的定义和相关用处<br>字段定义是这样的:<br>看到图中有这些字段,对主要字段说明:</blockquote>
<ul>
<li>菜单编码(对应前端页面的文件名,比如userMan, 渲染时就会找到 */userMan/index去resolve)</li>
<li>菜单组件 (指的是layout等,后面如果需要做多布局,通过这个设置页面即可有不同布局)</li>
</ul>
<p><img src="/img/bVbuoEg?w=755&h=506" alt="clipboard.png" title="clipboard.png"></p>
<pre><code>-- ----------------------------
-- bbs_menu
-- ----------------------------
DROP TABLE IF EXISTS `bbs_menu`;
CREATE TABLE `bbs_menu` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`pid` INT(11) DEFAULT '0',
`type` tinyint(4) NOT NULL DEFAULT '1' COMMENT '菜单类型: 1. 管理平台菜单 2. BBS菜单 3. 移动端菜单',
`code` VARCHAR(48) NOT NULL COMMENT '菜单编码',
`name` VARCHAR(48) NOT NULL COMMENT '菜单名称',
`component` tinyint(4) NOT NULL COMMENT '对应组件: -1. 根节点 1. 页面组件 2.默认布局 3456...扩展布局',
`icon` VARCHAR(128) DEFAULT NULL COMMENT '菜单图标',
`alias` VARCHAR(128) DEFAULT NULL COMMENT '别名',
`redirect` VARCHAR(128) DEFAULT NULL COMMENT '重定向路径: 配置菜单编码或URL',
`sort` INT(11) NOT NULL,
`desc` VARCHAR(128) DEFAULT NULL,
`status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '状态: 0:停用,1:启用(默认为1)',
`create_user` INT(11) DEFAULT NULL,
`create_time` datetime DEFAULT NULL,
`update_user` INT(11) DEFAULT NULL,
`update_time` datetime DEFAULT NULL,
`delete_user` INT(11) DEFAULT NULL,
`delete_time` datetime DEFAULT NULL,
`flag` tinyint(4) NOT NULL DEFAULT '1' COMMENT '状态: 0:删除,1:可用(默认为1)',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='菜单表';</code></pre>
<pre><code> id: '', // *唯一ID
pid: '', // *父ID
type: '', // *菜单类型
code: '', // *菜单编码
name: '', // *菜单名称
component: '', // *菜单组件
icon: '', // 菜单图标
redirect: '', // 重定向路径
sort: '', // *排序
desc: '', // 描述
status: 1 // *状态: 0:停用,1:启用(默认为1)'</code></pre>
<p>有什么用处呢和好处呢,就个人而言,就是觉得把路由表放在数据库,让项目更易于维护,在页面中通过一个匹配逻辑,可以将所有字段组装成为可以使用的路由表:</p>
<pre><code>// 得到页面路径
function getPath (arr, child, code) {
const pItem = arr.find(item => child.pid === item.id)
// 当前元素还存在父节点, 且父节点不为根节点
if (arr.find(item => pItem.pid === item.id && item.pid > -1)) {
getPath(arr, pItem, `${pItem.code}/${code}`)
} else {
return `${pItem.code}/${code}`
}
}
// 对基础数据的处理
item.meta = {}
item.meta.title = item.name
item.meta.icon = item.icon
item.meta.id = item.id
// 使路由名字具有唯一性
item.name = item.name + index
// 设置对应的页面路径
item.path = '/' + item.code
// 设置页面对应的组件 对应组件: -1. 根节点 1. 页面组件 2.默认布局 3456...扩展布局
switch (item.component) {
case -1:
console.log('根节点,已经过滤掉了')
break
case 1:
item.component = resolve => require([`@/views/${getPath(menu, item, item.code)}/index`], resolve)
break
case 2:
item.component = Layout
break
default:
item.component = resolve => require(['@/views/errorPage/401'], resolve)
break
}</code></pre>
<p>通过这种方式,在设置页面权限的时候,只需要接口设置当前角色对应的菜单,用户查询的时候能获取到的就是当前分配给他的权限,将这个权限组装成路由表,即可。</p>
<h4>数据权限</h4>
<p>上面说的是菜单的配置,以及生成。然后和每个页面相关的数据权限,需要点击到页面级别的菜单才可以访问到,如图:</p>
<p><img src="/img/bVbuoJn?w=1357&h=594" alt="clipboard.png" title="clipboard.png"></p>
<p>选中一个菜单之后,可以对这个菜单添加数据权限的控制,比如添加,编辑,删除等操作。</p>
<h5>数据权限的实现</h5>
<p>主要是字段设计,所以对图中字段(开发人员录入)详细说明:</p>
<ul>
<li>功能编码 (页面编码:功能编码,主要用于前端控制显隐)</li>
<li>功能api (接口编码,后端通过判断用户是否存在这个编码,来判断是否存在操作权限)</li>
<li>请求方式 (restfulApi情况下,因为api编码相同,需要根据请求方式来判断用户的操作权限)</li>
</ul>
<p><img src="/img/bVbuoJ0?w=753&h=366" alt="clipboard.png" title="clipboard.png"></p>
<h6>前端实现</h6>
<p>分配完权限之后,前端页面在对应的按钮或要操作的dom上,通过v-if 功能编码是否存在来设置操作权限的显示隐藏。<br>但是前端的显隐一旦用户绕过页面去访问接口即可,所以数据权限前端只是操作显隐,具体实现还在后端。</p>
<h6>后端实现</h6>
<ul>
<li>做一个数据权限中间层,用户访问时中间层判断当前访问的接口用户是否拥有权限</li>
<li>怎么判断,通过前端设置的功能api和请求方式,去表中查询当前用户角色是否可访问</li>
<li>可访问继续往下走,不能访问就拒绝了</li>
</ul>
<h3>角色管理</h3>
<p>用户存在了,菜单和数据权限也配置好了,但是需要角色去将他们关联到一起。</p>
<h4>绑定用户</h4>
<p>这里设置的逻辑是一个用户只能绑定一个角色。<br>角色管理页面,还是右键树组件,可以看到绑定用户的选项</p>
<p><img src="/img/bVbuoOj?w=1110&h=571" alt="clipboard.png" title="clipboard.png"></p>
<p><img src="/img/bVbuoO2?w=1300&h=486" alt="clipboard.png" title="clipboard.png"></p>
<h4>分配权限</h4>
<p>同样是右键,可以开始对角色进行分配权限的操作</p>
<p><img src="/img/bVbuoPm?w=1272&h=603" alt="clipboard.png" title="clipboard.png"></p>
<p>左边是页面的权限分配,选中页面之后,右边会出现数据权限的分配:</p>
<p><img src="/img/bVbuoPW?w=1335&h=671" alt="clipboard.png" title="clipboard.png"></p>
<h5>继承式的分配权限</h5>
<ul>
<li>总共有100个权限</li>
<li>a有50个,a给b分配时,只能分配50个</li>
<li>假设a给b分配了30个,c为b的下级,d为c的下级</li>
<li>c此时无权限,a或b能分配30个给c,但由于c无权限,a或b分配给d时,分配的列表为空</li>
</ul>
<h3>总结</h3>
<p>创建用户<br>创建菜单<br>创建角色<br>用户绑定角色,角色分配权限<br>完成</p>
<h2>最后</h2>
<p><a href="https://link.segmentfault.com/?enc=oEmZzCKp44LA5OLrvUJNCQ%3D%3D.%2Be591IGHdUSaDGSykFKcEeTusbWIQDAXKHGS7BQerE0%3D" rel="nofollow">案例地址</a></p>
<p><a href="https://link.segmentfault.com/?enc=fgK4VWyhauhsr6kK5828yQ%3D%3D.IawoDPWWUuRcMqKDQMrSFB1SmG4gOU3aJdh07GV%2Bubv3zSD2hkAg95KVYp1oEs7s" rel="nofollow">node服务</a></p>
实现node日志管理
https://segmentfault.com/a/1190000019482834
2019-06-14T16:46:36+08:00
2019-06-14T16:46:36+08:00
我在长安长安
https://segmentfault.com/u/wozaichanganchangan
7
<p>第一次写node项目,之前除了前端的脚手架构建接触过一些简单的,所以总是碰到很多坑。比如权限验证,比如异常处理,比如日志管理。<br>在看log4js使用方法的时候突然想到自己就可以实现简单的业务,不需要借助组件,虽然简单但是实现了挺开心的。</p>
<h2>为什么需要日志管理</h2>
<p>自己的node项目写了一段时间了,但一直没有加上日志管理的功能,因为觉得没必要,很多时候都是在自己电脑上面调试的。<br>但突然有一天在线上访问自己的项目,发现页面报错了,想知道为什么报错了,发现竟然没有什么很好的方法,如果我没有通过一个东西去记录的话,所以日志管理这个时候就显得尤为重要了。</p>
<h2>日志的产生过程</h2>
<ul>
<li>页面出现错误</li>
<li>根据错误类型创建日志文件</li>
<li>写入错误信息</li>
</ul>
<h2>创建日志方法的实现</h2>
<ul>
<li>先判断要写入的路径是否存在,不存在则创建</li>
<li>判断日志要创建在的文件夹存不存在,不存在则创建</li>
<li>判断当前要创建的日志存不存在,存在继续写入,不存在则创建并写入</li>
</ul>
<h3>fs.stat</h3>
<p>检查路径是否存在</p>
<h3>fs.mkdir</h3>
<p>创建目录的方法</p>
<h3>fs.readFile</h3>
<p>读取文件的方法</p>
<h3>fs.writeFile</h3>
<p>写入文件的方法</p>
<h3>完成的写入日志函数</h3>
<p>我的业务是定义了两个类型,错误和sql,然后传入日志内容</p>
<pre><code>/**
* 写入日志
* @param {String} type // 日志类型 err 错误日志 sql sql日志
* @param {String} content
*/
writeLog (content, type = 'err') {
// 创建不存在的文件夹
await this.dirExists(`log/file/${type}`)
// 获取到文件files
fs.readFile(`log/file/${type}/${utils.switchTime(new Date(), 'YYYY-MM-DD')}.log`, (err, data) => {
if (err) {
console.log(err)
}
// 写入文件
fs.writeFile(`log/file/${type}/${utils.switchTime(new Date(), 'YYYY-MM-DD')}.log`, `${data || ''}\n${content}`, async (err) => {
if (err) {
console.log(err)
}
})
})
}</code></pre>
<h2>使用</h2>
<h3>在sql执行函数上使用</h3>
<pre><code>function query (sql) {
// 写入sql
NodeLog.writeLog(sql, 'sql')
return new Promise((resolve, reject) => {
pool.getConnection((err, conn) => {
if (err) {
// 如果是连接断开,自动重新连接
if (err.code === 'PROTOCOL_CONNECTION_LOST') {
setTimeout(query(), 2000);
reject('断开重连');
} else {
console.error(err.stack || err);
reject(err);
}
} else {
// 得到结果
conn.query(sql, (queryErr, result) => {
if (queryErr) {
reject(queryErr);
} else {
resolve(result);
}
// 释放连接
conn.release();
})
}
})
})
}</code></pre>
<h3>在异常处理函数中使用</h3>
<pre><code> handleException (req, res, e) {
// 写入日志
NodeLog.writeLog(e)
res.json({
code: e.errno || 20501,
success: false,
content: e,
message: '服务器内部错误'
})
}</code></pre>
<h2>最后</h2>
<p><a href="https://link.segmentfault.com/?enc=eu%2B2pNnLm%2FXPfa256jw60g%3D%3D.tcQKHap1Vt%2BRW1GIHVLakofPSTy8KOyEeEfwCwxmfJF2Os3AoMYdI%2FiV%2Bb2Fb1Cmtc2Kq95e%2FR1UXFyN4gYKJg%3D%3D" rel="nofollow">日志完整代码</a></p>
<p><a href="https://link.segmentfault.com/?enc=Sal37WMEmmm%2FJTyZt73Xcw%3D%3D.DI1k%2FJaXkJe2NMgG%2BY2nprUEmqRq0ftqd%2BGIWanE4yD2JEDql%2FpPbDec68ceFL4o" rel="nofollow">node项目</a><br><a href="https://link.segmentfault.com/?enc=3SYnVrXNx3QGoeKd2d4Jdg%3D%3D.VqVvu1mj65Mxkc82TgGdTLUmY%2BP1PTT5GnV66wMSzUnQ9xnR0jXqmcWlPGKqGGaM" rel="nofollow">对应的前端项目</a></p>
前端工具函数
https://segmentfault.com/a/1190000019256898
2019-05-22T11:01:54+08:00
2019-05-22T11:01:54+08:00
我在长安长安
https://segmentfault.com/u/wozaichanganchangan
26
<h2>将一级的数据结构处理成树状数据结构</h2>
<blockquote>处理成树状结构,一般就是需要节点和父节点标识,或者需要考虑以哪个节点为根节点生成树结构数据</blockquote>
<pre><code>// 使用示例代码:
list: [{id: 1, pid: 0, name: 11}, {id: 2, pid: 1, name: 2}]
getTreeArr({ key: 'id', pKey: 'pid', data: list })
result: [
{id: 1, pid: 0, name: 11, children: [
{id: 2, pid: 1, name: 2}
]}
]</code></pre>
<pre><code> /**
* 将一级的数据结构处理成树状数据结构
* @param {Object} obj {key, pKey, data}
* @param obj.key 字段名称 比如id
* @param obj.pKey 父字段名称 比如 pid
* @param obj.rootPValue 根节点的父字段的值
* @param obj.data 需要处理的数据
* @param obj.jsonData 是否深复制数据(默认是true)
* @return {Array} arr
*/
getTreeArr: (obj) => {
if (!Array.isArray(obj.data)) {
console.log('getTreeArr=>请传入数组')
return []
}
obj.jsonData = obj.jsonData === false ? obj.jsonData : true
const arr = obj.jsonData ? JSON.parse(JSON.stringify(obj.data)) : obj.data
const arr1 = []
// 将数据处理成数状结构
arr.forEach(item => {
let index = 0
item.children = []
arr.forEach(item1 => {
// 得到树结构关系
if (item[obj.key] === item1[obj.pKey]) {
item.children.push(item1)
}
// 判断根节点
if (item1[obj.key] !== item[obj.pKey]) {
index++
}
})
// 没传入根节点,根据当前数据结构得到根节点
if (!('rootPValue' in obj) && index === arr.length) {
arr1.push(item)
}
})
// 传入根节点,根据传入的根节点组成树结构
if ('rootPValue' in obj) {
arr.forEach(item => {
if (item[obj.pKey] === obj.rootPValue) {
arr1.push(item)
}
})
}
return arr1
}</code></pre>
<h2>数组去重</h2>
<blockquote>数组去重方法有许多,还分为普通数组和对象数组,这里列举了一些,并把其中优缺点分析了一下</blockquote>
<pre><code> /**
* 数组去重
* @param {Array} data 要去重的数组
* @param {String} key 作为去重依据的字段 (处理对象数组时需要传入)
* @return arr 返回处理后的数据
*/</code></pre>
<h3>根据对象的属性不同去重</h3>
<p>推荐使用</p>
<pre><code> handleRepeatArr ({ data, key }) {
if (!Array.isArray(data)) {
console.log('请传入数组')
return
}
const arr = []; const obj = {}
data.forEach((item, index) => {
const attr = key ? item[key] : item
if (!obj[attr]) {
obj[attr] = index + 1
arr.push(item)
}
})
return arr
}</code></pre>
<h3>递归去重</h3>
<p>缺点:会将数据默认排序</p>
<pre><code> handleRepeatArr ({ data, key }) {
if (!Array.isArray(data)) {
console.log('请传入数组')
return
}
/** 1.递归去重,缺点,会将数据默认排序 */
// 先对数据做排序处理
data = data.sort((item, item1) => {
if (key) {
return item[key] - item1[key]
}
return item - item1
})
// 递归去重
function getData (index) {
if (index >= 1) {
// 判断当前数据和下一条数据是否相等
let result = key ? data[index][key] === data[index - 1][key] : data[index] === data[index - 1]
if (result) {
data.splice(index, 1)
}
getData(index - 1)
}
}
getData(data.length - 1)
return data
}</code></pre>
<h3>利用indexOf以及forEach</h3>
<p>缺点:适合处理数组,不适合处理对象数组</p>
<pre><code> handleRepeatArr ({ data, key }) {
if (!Array.isArray(data)) {
console.log('请传入数组')
return
}
let arr = []
data.forEach((item, index) => {
// 如果当前元素在之后没有出现过(后面出现的数据会保留)
// let result = data.indexOf(item, index + 1)
// 如果当前元素在之前没有出现过(前面出现的数据会保留)
let result = index === 0 ? -1 : data.lastIndexOf(item, index - 1)
if (result === -1) {
arr.push(item)
}
})
return arr
}</code></pre>
<h3>new Set</h3>
<p>缺点:适合处理数组,不适合处理对象数组</p>
<pre><code>return [...new Set(data)]</code></pre>
<h3>双层循环去重</h3>
<p>缺点:占用内存高</p>
<pre><code> handleRepeatArr ({ data, key }) {
if (!Array.isArray(data)) {
console.log('请传入数组')
return
}
for (let i = 0, len = data.length; i < len; i++) {
for (let j = i + 1; j < len; j++) {
let result = key ? data[i][key] === data[j][key] : data[i] === data[j]
if (result) {
data.splice(j, 1)
len--
j--
}
}
}
return data
}</code></pre>
<h2>复制内容</h2>
<p>复制成功后如果需要提示,需要自定义相关回调,当前函数使用的是element-ui的弹窗</p>
<pre><code> /**
* 复制
* @param {String} value 要复制的值
*/
copyData (value) {
const inputDom = document.createElement('input')
inputDom.value = value
document.body.appendChild(inputDom)
inputDom.select() // 选择对象
document.execCommand('Copy') // 执行浏览器复制命令
document.body.removeChild(inputDom) // 删除DOM
Message({
type: 'success',
message: '复制成功'
})
}</code></pre>
<h2>a模拟window.open打开窗口</h2>
<p>因为有些浏览器会默认拦截window.open,当需要函数中打开窗口,可以使用a标签模拟window.open</p>
<pre><code> /**
* a模拟window.open,不会被浏览器拦截
* @param {String} url a标签打开的地址
* @param {String} id a标签的ID
* @param {String} targetType a标签点击打开的方式(当前页面打开还是新窗口打开)
*/
openWindow: (url, targetType = '_blank', id = 'open', download = false) => {
// 如果存在则删除
if (document.getElementById(id)) {
document.body.removeChild(document.getElementById(id))
}
const a = document.createElement('a')
a.setAttribute('href', url)
if (download) {
a.setAttribute('download', url)
}
a.setAttribute('target', targetType)
a.setAttribute('id', id)
document.body.appendChild(a)
a.click()
}</code></pre>
<h2>得到想要的时间格式</h2>
<p>这个在业务中用的比较频繁</p>
<pre><code>// 使用示例代码:
switchTime(new Date(), 'YYYY-MM-DD hh') // 返回 2019-05-22 11
switchTime(new Date(), 'YYYYMMDD hh:mm:ss') // 返回 20190522 11:00:00</code></pre>
<pre><code> /**
* 传入时间戳,转换指定的时间格式
* @param {Number} val 时间戳
* @param {String} dateType 要得到的时间格式 例如 YYYY-MM-DD hh:mm:ss
* @return dataStr 例如 YYYY-MM-DD hh:mm:ss
*/
switchTime: (val = +new Date(), dateType = 'YYYY-MM-DD hh:mm:ss') => {
// 将字符串转换成数字
const timeStamp = +new Date(val)
// 如果转换成数字出错
if (!timeStamp) {
return val
}
let str
// 得到时间字符串
const dateStr = new Date(timeStamp)
str = dateType.replace('YYYY', dateStr.getFullYear())
str = str.replace('MM', (dateStr.getMonth() + 1 < 10 ? '0' : '') + (dateStr.getMonth() + 1))
str = str.replace('DD', (dateStr.getDate() < 10 ? '0' : '') + dateStr.getDate())
str = str.replace('hh', (dateStr.getHours() < 10 ? '0' : '') + dateStr.getHours())
str = str.replace('mm', (dateStr.getMinutes() < 10 ? '0' : '') + dateStr.getMinutes())
str = str.replace('ss', (dateStr.getSeconds() < 10 ? '0' : '') + dateStr.getSeconds())
return str
}</code></pre>
<h2>时间显示转换</h2>
<p>这个方法中需要应用到上一个方法,获取当前时间,或者可以自行得到时间然后再去处理</p>
<pre><code>假设当前时间为 2019-05-20 00:00:00
// 使用示例代码:
timeView(new Date()) // 刚刚发布
timeView('2019-05-19 23:01:00') // 59分钟前
timeView('2019-05-19 12:00:00') // 12小时前
timeView('2019-05-15 12:00:00') // 5天前
timeView('2019-04-15 12:00:00') // 04-15
timeView('2018-04-15 12:00:00') // 2018-04-15</code></pre>
<pre><code> /**
* 时间显示
*/
timeView: function (val) {
const now = +new Date() // 当时时间
const timeStamp = +new Date(val) // 需要处理的时间
const result = now - timeStamp // 相差的时间戳
const min = 60 * 1000 // 分钟的毫秒数
const hour = 60 * 60 * 1000 // 小时的毫秒数
const day = 60 * 60 * 1000 * 24 // 日的毫秒数
if (result / min < 1) {
return '刚刚发布'
} else if (result / min < 60) {
return Math.floor(result / min) + '分钟前'
} else if (result / hour > 1 && result / hour < 24) {
return Math.floor(result / hour) + '小时前'
} else if (result / day > 1 && result / day < 7) {
return Math.floor(result / day) + '天前'
} else if (this.switchTime(now, 'YYYY') === this.switchTime(timeStamp, 'YYYY')) {
return this.switchTime(timeStamp, 'MM月DD日')
} else {
return this.switchTime(timeStamp, 'YYYY年MM月DD日')
}
}</code></pre>
<h2>处理搜索栏参数</h2>
<pre><code> getLocationSearch () {
const str = window.location.search
const arr = str.substr(1).split('&')
const obj = {}
for (const item of arr) {
const data = item.split('=')
obj[data[0]] = data[1]
}
return obj
}</code></pre>
<h2>文件大小显示转换</h2>
<pre><code> bytesToSize (bytes) {
if (bytes === 0) return '0 B'
var k = 1024 // or 1024
var sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
var i = Math.floor(Math.log(bytes) / Math.log(k))
return (bytes / Math.pow(k, i)).toPrecision(3) + ' ' + sizes[i]
}</code></pre>
<h2>对请求失败的HTTP状态码做处理</h2>
<pre><code> /**
* 对请求失败的HTTP状态码做处理
* @param {Number} code HTTP状态码
* @param {String} message 错误提示
* @return message 返回处理过的提示信息
*/
requestError: (code, message) => {
const statusCode = (code + '').replace(/[^0-9]+/g, '') - 0
switch (statusCode) {
case 400:
return 'Bad Request (错误的请求)'
case 401:
return 'Unauthorized (请求要求身份验证)'
case 403:
return 'Forbidden (服务器拒绝请求)'
case 404:
return 'NOT Found (服务器找不到请求的资源)'
case 405:
return 'Bad Request (禁用请求中指定的方法)'
case 406:
return 'Not Acceptable (无法使用请求的内容特性响应请求的网页)'
case 407:
return 'Proxy Authentication Required (需要代理授权)'
case 408:
return 'Request Timed-Out (服务器等候请求时发生超时)'
case 409:
return 'Conflict (服务器在完成请求时发生冲突。服务器必须在响应中包含有关冲突的信息)'
case 410:
return 'Gone (请求的资源已被永久删除)'
case 411:
return 'Length Required (服务器不接受不含有效内容长度标头字段的请求)'
case 412:
return 'Precondition Failed (未满足前提条件)'
case 413:
return 'Request Entity Too Large (请求实体过大)'
case 414:
return 'Request, URI Too Large (请求的 URI 过长)'
case 415:
return 'Unsupported Media Type (不支持的媒体类型)'
case 429:
return '您的操作过于频繁,请稍后重试'
case 500:
return 'Internal Server Error (服务器内部错误)'
case 501:
return 'Not Implemented (尚未实施)'
case 502:
return 'Bad Gateway (错误网关)'
case 503:
return 'Server Unavailable (服务不可用)'
case 504:
return 'Gateway Timed-Out (网关超时)'
case 505:
return 'HTTP Version not supported (HTTP 版本不受支持)'
default:
return message
}
}</code></pre>
<h2>通过key找到在列表中对应的名字</h2>
<pre><code>// 使用示例代码:
list: [{key: '红色', value: 1}]
getDataName({dataList: list, value: 'value', label: 'key', data: 1}) // 红色</code></pre>
<pre><code> /**
* 通过key找到在列表中对应的显示
* @param {Object} obj
* @param obj.dataList 数据列表
* @param obj.value 数据的值对应的字段名称 例如 'value'
* @param obj.label 数据的说明对应的字段名称 例如 'label'
* @param obj.data 当前传入的数据值
* @return name 返回当前传入值在数组中对应的名字
*/
getDataName: (obj) => {
let name = obj.data
if (Array.isArray(obj.dataList) && obj.dataList.length > 0) {
for (let i = 0; i < obj.dataList.length; i++) {
if (obj.dataList[i][obj.value] === obj.data) {
name = obj.dataList[i][obj.label]
}
}
}
return name
}</code></pre>
<h2>代码地址</h2>
<p><a href="https://link.segmentfault.com/?enc=9ELDvN%2BYLi0ZROfeG%2Bcncg%3D%3D.DlXxvyY%2FnR4q4RR97o4dbvcaouVnQpEna7BeG93soXd5NvCmd%2BCss%2F4ypDMaQLZfqXYHUywCKaF6fmTw%2FJpg5jK0Jd89UVNSC68nJt2Oqfg%3D" rel="nofollow">工具库地址</a></p>
组件化页面:封装el-form
https://segmentfault.com/a/1190000019129799
2019-05-09T21:47:00+08:00
2019-05-09T21:47:00+08:00
我在长安长安
https://segmentfault.com/u/wozaichanganchangan
26
<blockquote>目前在编写个人项目,有一个是管理平台,基本每个页面都有el-from,所以对el-form做了二次封装, 组件在个人开发使用不错,但不确定能满足各种业务需求,所以这里主要和大家分享一下设计思路。</blockquote>
<h2>设计组件</h2>
<h3>分析问题</h3>
<p>el-form是element-ui库的表单组件,如果我们要将其进行二次封装,那么需要考虑几个问题:</p>
<ul>
<li>如何设计表单渲染字段</li>
<li>不同类型的el-form-item怎么去渲染,比如input,select,或者自定义显示内容等</li>
<li>表单联动怎么去处理</li>
<li>事件绑定</li>
<li>表单验证</li>
<li>更多需求...</li>
</ul>
<p>下面通过这些点,来实现封装一个二次的el-form组件。</p>
<h2>从字段开始</h2>
<p><img src="/img/bVbsqtC?w=2052&h=1262" alt="图片描述" title="图片描述"></p>
<p>拿业务用到的表单来举例,在这个表单中的需求分析。</p>
<p>首先是el-form-item的类型:</p>
<ul>
<li>tag类型显示</li>
<li>input输入</li>
<li>select选择</li>
<li>按钮或者图片的显示或者绑定操作</li>
<li>textarea输入</li>
</ul>
<p>然后再分析每个节点:</p>
<ul>
<li>label宽度</li>
<li>是否需要验证</li>
<li>placeholder显示</li>
<li>验证规则</li>
<li>绑定的相关事件</li>
<li>是否可为readonly/disabled</li>
<li>节点的class/样式 (一行显示一个或者多个)</li>
</ul>
<p>初步分析,大概会有这些需求,接下来对单个问题一一来分析解决。</p>
<h3>设计渲染字段列表</h3>
<p>正常情况下,我们使用form,它的子项会是这样的,比如有input和select:</p>
<pre><code>// input类型
<el-form-item label="活动名称" prop="name">
<el-input v-model="ruleForm.name"></el-input>
</el-form-item>
// select类型
<el-form-item label="活动区域" prop="region">
<el-select v-model="ruleForm.region" placeholder="请选择活动区域">
<el-option label="区域一" value="shanghai"></el-option>
<el-option label="区域二" value="beijing"></el-option>
</el-select>
</el-form-item></code></pre>
<p>仔细一看,外面那一层壳是可以拿掉的,比如长这样:</p>
<pre><code><el-form-item label="label" prop="prop">
// 如果是input类型
<el-input v-if="input" v-model="ruleForm.name"></el-input>
// 如果是select类型
<el-select v-if="select" v-model="ruleForm.region" placeholder="请选择活动区域">
<el-option label="区域一" value="shanghai"></el-option>
<el-option label="区域二" value="beijing"></el-option>
</el-select>
</el-form-item></code></pre>
<p>那由此我们可以设计出循环form的字段列表:</p>
<ul>
<li>label (字段名)</li>
<li>value (字段值 prop,后面会有value代替)</li>
<li>type (字段类型 input/select/password/textarea等)</li>
</ul>
<p>然后除了上面的例子我们还可以自己扩展一些字段:</p>
<ul>
<li>event (绑定的方法)</li>
<li>list (列表 如果是select类型,需要有对于的list去渲染)</li>
<li>TimePickerOptions (时间配置 如果是时间类型,可以传入配置)</li>
<li>disabled (是否禁止)</li>
<li>filterable (是否可筛选)</li>
<li>clearable (是否可清除)</li>
<li>required (是否必填 根据这个字段,去设置对于的验证规则)</li>
<li>validator (自定义验证 验证时将使用自定义验证方法)</li>
<li>show (是否显示, 布尔值或者是函数,下面会对联动渲染详细分析)</li>
<li>更多(根据需求和场景扩展更多字段)</li>
</ul>
<p>然后完整的字段配置列表大概是这样的(个人使用场景,不需要使用到所有的设计字段):</p>
<pre><code> fieldList: [
{ label: '账号', value: 'account', type: 'input', required: true, validator: checkAccount },
{ label: '密码', value: 'password', type: 'password', required: true, validator: checkPwd },
{ label: '昵称', value: 'name', type: 'input', required: true },
{ label: '性别', value: 'sex', type: 'select', list: 'sexList', required: true },
{ label: '头像', value: 'avatar', type: 'slot', className: 'el-form-block' },
{ label: '手机号码', value: 'phone', type: 'input', validator: checkPhone },
{ label: '微信', value: 'wechat', type: 'input', validator: checkWechat },
{ label: 'QQ', value: 'qq', type: 'input', validator: checkQQ },
{ label: '邮箱', value: 'email', type: 'input', validator: checkEmail },
{ label: '描述', value: 'desc', type: 'textarea', className: 'el-form-block' },
{ label: '状态', value: 'status', type: 'select', list: 'statusList', required: true }
]</code></pre>
<p>组件内部怎么操作,很简单,根据规则,一一对应循环字段,绑定属性就ok了,所以组件内部template就是这么点代码:</p>
<pre><code> <el-form
ref="form"
class="page-form"
:class="className"
:model="data"
:rules="rules"
:label-width="labelWidth"
>
<el-form-item
v-for="(item, index) in getConfigList()"
:key="index"
:prop="item.value"
:label="item.label"
:class="item.className"
>
<!-- solt -->
<template v-if="item.type === 'slot'">
<slot :name="'form-' + item.value" />
</template>
<!-- 普通输入框 -->
<el-input
v-if="item.type === 'input' || item.type === 'password'"
v-model="data[item.value]"
:type="item.type"
:disabled="item.disabled"
:placeholder="getPlaceholder(item)"
@focus="handleEvent(item.event)"
/>
<!-- 文本输入框 -->
<el-input
v-if="item.type === 'textarea'"
v-model.trim="data[item.value]"
:type="item.type"
:disabled="item.disabled"
:placeholder="getPlaceholder(item)"
:autosize="{minRows: 2, maxRows: 10}"
@focus="handleEvent(item.event)"
/>
<!-- 计数器 -->
<el-input-number
v-if="item.type === 'inputNumber'"
v-model="data[item.value]"
size="small"
:min="item.min"
:max="item.max"
@change="handleEvent(item.event)"
/>
<!-- 选择框 -->
<el-select
v-if="item.type === 'select'"
v-model="data[item.value]"
:disabled="item.disabled"
:clearable="item.clearable"
:filterable="item.filterable"
:placeholder="getPlaceholder(item)"
@change="handleEvent(item.event, data[item.value])"
>
<el-option
v-for="(childItem, childIndex) in listTypeInfo[item.list]"
:key="childIndex"
:label="childItem.key"
:value="childItem.value"
/>
</el-select>
<!-- 日期选择框 -->
<el-date-picker
v-if="item.type === 'date'"
v-model="data[item.value]"
:type="item.dateType"
:picker-options="item.TimePickerOptions"
:clearable="item.clearable"
:disabled="item.disabled"
:placeholder="getPlaceholder(item)"
@focus="handleEvent(item.event)"
/>
<!-- 信息展示框 -->
<el-tag v-if="item.type === 'tag'">
{{ $fn.getDataName({dataList: listTypeInfo[item.list], value: 'value', label: 'key', data: data[item.value]}) || '-' }}
</el-tag>
</el-form-item>
</el-form></code></pre>
<h2>自定义和联动</h2>
<p>通过上面的操作,我们完成了基本的表单动态渲染,但是只是针对于模版型的场景,举个例子,如果表单中我要显示选择头像,或者需要增加一个第三方的控件,这个时候,上面的渲染规则就有些无能威力了,所以我们需要表单组件支持自定义渲染。</p>
<h3>slot-组件自定义神器</h3>
<p>不了解的同学请戳这个<a href="https://link.segmentfault.com/?enc=%2FEhSD8Q%2BRWwH%2BU19mKgqcg%3D%3D.qKDOHCZvEPE2nEus8jsziPf%2B4HBPL2JvsPaGJAIylOlYwHMQgU68Pge1PrLQM0MLEFtJdrnvUd6ONlviC7eaGg%3D%3D" rel="nofollow">vue中的slot属性</a><br>在上面,组件的代码中有这样一段:</p>
<pre><code> <!-- solt -->
<template v-if="item.type === 'slot'">
<slot :name="'form-' + item.value" />
</template></code></pre>
<p><strong>这段代码的意思是:</strong>渲染类型为slot,插槽的名称为‘form-’前缀加上字段的值。<br>有什么用呢?我们回到使用组件的页面。</p>
<p><img src="/img/bVbsqzo?w=1904&h=950" alt="图片描述" title="图片描述"></p>
<p>在form中,这里有一个子项,需要显示图片和按钮,这个时候组件内部已经定义了插槽,并且有对于的名字,我们只需要在外部将对应的插槽传入到组件中,即可实现自定义内容,请看对应代码:</p>
<pre><code> <!-- 自定义插槽-选择头像 -->
<template v-slot:form-avatar>
<div class="slot-avatar">
<img
v-imgAlart
:src="formInfo.data.avatar"
style="height: 30px;"
>
<el-button
v-waves
type="primary"
icon="el-icon-picture"
size="mini"
@click="handleClick('selectFile')"
>
{{ formInfo.data.avatar ? '更换头像' : '选择头像' }}
</el-button>
</div>
</template>
</page-form></code></pre>
<p><strong>组件内插槽除了可以接收对应名字的内容外,还可以将组件中的数据通过插槽传输到外部,在外部使用组件数据渲染内容,自定义组件神器,请务必了解</strong></p>
<h3>表单联动</h3>
<p>表单联动,推荐阅读element文章---<a href="https://link.segmentfault.com/?enc=htGD%2FWg1DcQuBdpe04tUkA%3D%3D.N6GD%2FhMSsb8jjcxJWyryeZzlQxsNpzy8vT7M9D%2FKGcor3rU7xp6pgZASm3cdvzs0" rel="nofollow">再也不想写表单了</a><br>表单组件,重要点之一就是表单联动了,我们来分析一下联动的场景:</p>
<ol>
<li>创建用户时,账号可以填写,编辑用户时,账号只能查看</li>
<li>创建时表单显示的字段只有三个,编辑时可查看到字段为十个</li>
<li>单选框,选择A,AA字段消失,选择B, AA,BB字段消失,选择C,所有字段都显示,并且变成必填</li>
<li>更多联动场景...</li>
</ol>
<h4>显示联动</h4>
<p><strong>1.字段定义为布尔值时</strong></p>
<p>在字段定义的时候,有定义一个字段,show,我们可以将show字段定义为布尔值,然后在页面中watch定义数据的是否显示,比如这段代码:</p>
<pre><code> // 根据弹窗类型做数据联动
'dialogInfo.type' (val) {
const fieldList = this.formInfo.fieldList
switch (val) {
case 'userInfo':
fieldList.forEach(item => {
if (['user_old_pwd', 'user_new_pwd', 'user_new1_pwd'].includes(item.value)) {
item.show = true
} else {
item.show = false
}
})
break
case 'password':
fieldList.forEach(item => {
if (!['user_old_pwd', 'user_new_pwd', 'user_new1_pwd'].includes(item.value)) {
item.show = true
} else {
item.show = false
}
})
break
}
}</code></pre>
<p><strong>2.字段定义为函数时</strong></p>
<p>如果不想再组件外部操作,想把显示隐藏的渲染逻辑交给组件内部处理,那我们可以定义show字段为函数,比如这段代码:</p>
<pre><code>{label: '名称', value: 'name', show: data => {
return data.type === 'userInfo'
}}</code></pre>
<p>这两种方式是我目前自定义组件场景中有用到的,根据需求,使用不同的方法</p>
<h4>逻辑联动和事件中间件设计</h4>
<p><strong>1. 字段定义为函数时</strong><br>逻辑联动表示form中某一项发生改变,其他一项或者多项会根据对应的数据发生相对的改变,比如下面这段代码(因为个人项目目前没有这种业务,所以并没有相关示例,代码来自element博客):</p>
<pre><code>[
{
title: '活动类型',
key: 'act_type',
type: 'radio',
props: {
options: { 1: '拉新', 2: '冲单', 3: '回馈' }
}
},
{
title: '生效方式',
key: 'effect_type',
type: 'radio',
props(form) {
const value;
const map = { 1: '立即', 2: '按时间', 3: '按条件' };
if (form.act_type === 3) {
value = 1;
}
return {
value: value,
options: map
};
}
}
]</code></pre>
<p>将需要联动的字段定义为方法,方法接收表单数据,方法根据数据进行对应的联动,这种方法可以基本完美解决各种问题,在组件内部则需要对属性添加一层判断,普通类型只需要进行绑定,typeof为function需要绑定运行的函数。</p>
<p><strong>2. 定义事件字段,通过中间件派发到外部处理</strong><br>定义事件字段,定义的字段列表中,我们可以设计一个event字段,当事件上绑定方法的时候,字段设计为这样:</p>
<pre><code>{ label: '性别', value: 'sex', type: 'select', list: 'sexList', event: 'changeName', required: true }</code></pre>
<p>示例代码为select类型,组件的input绑定代码上也需要绑定相关事件:</p>
<pre><code> <!-- 选择框 -->
<el-select
v-if="item.type === 'select'"
v-model="data[item.value]"
:disabled="item.disabled"
:clearable="item.clearable"
:filterable="item.filterable"
:placeholder="getPlaceholder(item)"
@change="handleEvent(item.event, data[item.value])"
>
<el-option
v-for="(childItem, childIndex) in listTypeInfo[item.list]"
:key="childIndex"
:label="childItem.key"
:value="childItem.value"
/>
</el-select></code></pre>
<p>通过handleEvent中间件,绑定对应类型和对应数据,中间件函数的用处就负责进行页面和组件的通讯,组件内部穿出事件类型和数据,页面接收并且处理,不论多少的事件,只有一个中间件即可以解决。<br>组件内部中间件处理:</p>
<pre><code> // 绑定的相关事件
handleEvent (evnet) {
this.$emit('handleEvent', evnet)
}</code></pre>
<p>组件使用代码:</p>
<pre><code> <!-- form -->
<page-form
v-if="dialogInfo.type !== 'userTransfer'"
:ref-obj.sync="formInfo.ref"
:data="formInfo.data"
:field-list="formInfo.fieldList"
:rules="formInfo.rules"
:label-width="formInfo.labelWidth"
:list-type-info="listTypeInfo"
@handleClick="handleClick"
@handleEvent="handleEvent"
>
<!-- 自定义插槽-选择头像 -->
<template v-slot:form-avatar>
<div class="slot-avatar">
<img
v-imgAlart
:src="formInfo.data.avatar"
style="height: 30px;"
>
<el-button
v-waves
type="primary"
icon="el-icon-picture"
size="mini"
@click="handleClick('selectFile')"
>
{{ formInfo.data.avatar ? '更换头像' : '选择头像' }}
</el-button>
</div>
</template>
</page-form></code></pre>
<p>页面中处理事件:</p>
<pre><code> // 触发事件
handleEvent (event, data) {
switch (event) {
case 'changeName':
console.log(data)
// 触发相关联动逻辑
break
}
}</code></pre>
<p>两种方式,各有优劣,这里主要分享思路,大家根据自己业务选择合适的方案。</p>
<h2>表单验证处理</h2>
<p>表单验证,戳这个,上次写的验证还热乎的----><a href="https://link.segmentfault.com/?enc=eTsb%2FzjMZffoAFW55Efy6g%3D%3D.%2BhdFL5Rum2F%2F%2F6KJDD9OlUFtzkmk8wo%2B9XmwrwEFG4RwATdMCG12jmQyDLX%2Botkp" rel="nofollow">element-ui表单全局验证的方法</a></p>
<h2>最后</h2>
<p><a href="https://link.segmentfault.com/?enc=RzqqtQmMBdD0lGchL%2B%2FCNA%3D%3D.2zdKmZgTLpQZaCrl0BHnYApnqRhpWBN5s3dew7DQh8M%3D" rel="nofollow">项目地址</a></p>
<p><a href="https://link.segmentfault.com/?enc=Px%2BFu5Dxu0vadvRa97YBnA%3D%3D.Guf5UoErl4JtUyHDrWHCUvRLw7JJUmrrs%2BXmE8msX5RK%2B4engH%2FraGuWksZgLB2bZaSy4Shs2v5KjLDewptZChaNJWhzx%2BWd%2F6FmVCbha4Q%3D" rel="nofollow">组件地址</a></p>
<h3>相关文章</h3>
<p><a href="https://link.segmentfault.com/?enc=kN%2Fujltz4N4Z1LBk9I6SIw%3D%3D.MB6Ev3y4IoRJHeoX%2BaBrtOQWsxHb%2BUm2ZjzRnYfMYDb9GIo6pTH6f2RWVJWujm5V" rel="nofollow">实现elementUI表单的全局验证</a></p>
<p><a href="https://link.segmentfault.com/?enc=o7CQKQjwaTtMiRQa9zUkcg%3D%3D.9XCLskE5LfE3JS9URyE0Q6%2FjL61tcGMtfz06s0FlZPlj7OfoQAPwCqB5H051v5N%2F" rel="nofollow">组件化页面:封装el-table</a></p>
组件化页面:封装el-table
https://segmentfault.com/a/1190000019116676
2019-05-08T23:15:59+08:00
2019-05-08T23:15:59+08:00
我在长安长安
https://segmentfault.com/u/wozaichanganchangan
11
<p>项目做的越来越多,重复的东西不断的封装成了组件,慢慢的,页面就组件化了,只需要定义组件配置数据,使用就好了,这是一件非常舒服的事情,这篇文章主要和大家讲讲如何对element-ui中的el-table进行二次封装。</p>
<h2>分析需求</h2>
<p>公有组件,可以被任何页面调用,首先要保证组件不和外部业务发生耦合,其次就是要设计合理的字段,使用时通过不同的配置即可使用。<br>那先大致来分析以下可能有的需求:</p>
<ul>
<li>动态表头</li>
<li>嵌套表头</li>
<li>表格显示内容类型自定义(文字,图片,超链接等)</li>
<li>动态接口加载数据</li>
<li>表格和分页联动</li>
<li>分页和查询数据联动</li>
<li>表格事件的处理</li>
<li>className, width, height...</li>
<li>更多需求...</li>
</ul>
<p><em>目前封装的组件并不算完美,不可能满足所以需求,这里的话主要还是和大家分享思路</em></p>
<h2>动态表头和嵌套表头的实现</h2>
<p>实现动态表头,这个应该是许多使用table的朋友们的痛点,明明是一样的东西,却要写多个表格,实在不能忍,让我们今天来一举歼灭它。</p>
<h3>分析表头结构</h3>
<p>el-table表头有两个必须的属性,prop值和label名字,其他非必须的有fixed,align,width或者min-width等,那由此可设计出一个这样的数据结构:</p>
<pre><code>{
prop: 'name',
label: '名称',
fixed: true/false,
align: 'center',
minWidth: 160
}</code></pre>
<h3>进阶->嵌套表格</h3>
<p>上面我们得出了普通表头列的设计,那我们继续分析,看看嵌套表格配置多了哪些字段。<br>根据element-ui官网文档,可以看到前面字段基本一样,嵌套表格多了children字段,用来循环子级表头,那由此我们可以设计出这样的数据结构:</p>
<pre><code>{
prop: 'name',
label: '名称',
fixed: true/false,
align: 'center',
minWidth: 160,
children: [
{
prop: 'oldName',
label: '旧名称',
fixed: true/false,
align: 'center',
minWidth: 160,
},
{
prop: 'newName',
label: '新名称',
fixed: true/false,
align: 'center',
minWidth: 160,
}
]
}</code></pre>
<h3>表头设计总结</h3>
<blockquote>表头设计思路大概是这样,并不复杂,根据业务需求,大家都可以设计适合自己使用的字段。</blockquote>
<p>完整的表头设计字段应该大概会是这个样子这个是个人字段配置的例子,其中将prop字段改成了value, <strong>下面代码统一会使用value代替prop</strong>。</p>
<pre><code>fieldList: [
{ label: '账号', value: 'account' },
{ label: '用户名', value: 'name' },
{ label: '所属角色', value: 'role_name', minWidth: 120 },
{ label: '性别', value: 'sex', width: 80, list: 'sexList' },
{ label: '账号类型', value: 'type', width: 100, list: 'accountTypeList' },
{ label: '状态', value: 'status', width: 90, type: 'slot', list: 'statusList' },
{ label: '创建人', value: 'create_user_name' },
{ label: '创建时间', value: 'create_time', minWidth: 180 },
{ label: '更新人', value: 'update_user_name' },
{ label: '更新时间', value: 'update_time', minWidth: 180 }
]</code></pre>
<h2>表格显示内容类型自定义</h2>
<p>表头设计只是将一些基本的需求实现了,但是实际业务往往更为复杂,比如当前列要显示的是图片,tag,超链接,或者列的数据是一个id要显示对应的label。</p>
<h3>字段列表扩展</h3>
<p>之前定义的字段列表都是简单的文字显示,当有了不同的类型显示需求,则意味着需要一个类型字段,type,根据业务需求,可以设计满足image,tag,href等。<br>字段设计为type为image时,同时可以考虑设计width和height字段。<br>字段设计为href时,可以同时设计颜色,跳转方式字段。<br>比如:</p>
<pre><code>{label: '设备信息', prop: 'deviceInfo', type: 'href', herf: 'https://www.baidu.com', target: '_blank'},
{label: '设备图标', prop: 'deviceImage', type: 'image', src: 'https://www.baidu.com', height: '60px', width: 'auto'}</code></pre>
<p>当列的数据是一个id的时候需要显示对应的label,情况又稍微复杂了一点,多种实现方法:</p>
<ul>
<li>获取到表格数据后对数据做处理,这个比较简单,但需要在组件外部操作(不推荐)</li>
<li>将对应的列表传入组件中,在组件内部进行转换(推荐)</li>
<li>设置为slot(好用,但建议使用在复杂的自定义场景,这个在下面会细讲)</li>
</ul>
<p>讲讲第二种方式,将对应的列表传入组件中,在组件内部进行转换,需要设置当前字段的类型为id转换为label的类型,我在字段上定义的是type: select,然后要定义相关的list,字段设计大概长这样:</p>
<pre><code>{ label: '菜单组件', value: 'component', type: 'select', list: 'componentList1' }</code></pre>
<p>我的实现方式是定义了一个listType对象,然后把页面上用到的list都挂在了这个对象上面,将listType传入到table组件中,通过listType[item.list]可以获取到字段对应列表然后获取对应的label显示。</p>
<h3>slot</h3>
<p>非常非常非常重要的slot,特别提醒大家,如果想写复杂的组件,考虑到自定义类型,请一定去了解slot<a href="https://link.segmentfault.com/?enc=WX6MCaCiRBjIt8HMCY7e6w%3D%3D.4HZ6k2NyyyvPCwOQGy9QWI6JLYXqnIWmA4kD37bOSidvNmMi7jSjtKRAjDZpUyu25pUPuQnfDrECJpl4aItm%2Bw%3D%3D" rel="nofollow">不了解的请戳</a><br>vue2.6+已经废弃slot-scope<a href="https://link.segmentfault.com/?enc=j41Lc5WMItChUCEAamPwKw%3D%3D.d9Z4VDJNmJjOCrZDyGfhi5A6OUsPWE2JuVTXkV0aW4q9OrpJzXVwtrAzjnL%2FFUDl2ubgzN6sX6QYC%2BjDUzZ2zQ%3D%3D" rel="nofollow">官网api描述</a></p>
<h4>插槽</h4>
<ul>
<li>父级可以向组件内部传入dom,组件内部通过插槽接收</li>
<li>渲染方式1: dom使用父级数据渲染,传入组件</li>
<li>渲染方式2: dom使用组件内部插槽穿出的数据渲染,再传入组件</li>
</ul>
<h4>匿名插槽</h4>
<p>父级在使用组件的时候,在组件标签内编写内容,将会组件内部<solt><slot/>接收到</p>
<h4>具名插槽</h4>
<p>父级设置传入的插槽的名字,组件内部匹配到名字相同的插槽进行渲染。<br>组件内部具名插槽传输数据到父级(dom接收方,数据传出方):</p>
<pre><code> <!-- solt 自定义列-->
<template v-if="item.type === 'slot'">
<slot
:name="'col-' + item.value"
:row="scope.row"
/>
</template></code></pre>
<p>父级获取插槽数据渲染dom(dom传出方,数据接收方):</p>
<pre><code><!-- 自定义插槽显示状态 -->
<template v-slot:col-status="scope">
<i
:class="scope.row.status === 1 ? 'el-icon-check' : 'el-icon-close'"
:style="{color: scope.row.status === 1 ? '#67c23a' : '#f56c6c', fontSize: '20px'}"
/>
</template></code></pre>
<h4>总结</h4>
<p>slot是自定义组件的神器。<br>回到table组件,我们需要自定义显示内容,设计的字段应该如下:</p>
<pre><code>{ label: '菜单图标', value: 'icon', type: 'slot' }</code></pre>
<h2>动态接口加载数据</h2>
<blockquote>上面说的都是显示字段设计的东西,现在开始分析表格的数据,从哪里来,到哪里去。</blockquote>
<p>如果要偷懒,那么一定是要把懒偷到底的,有一丁点多余的工作要做,都是偷懒不成功的。</p>
<h3>组件内部加载数据</h3>
<p>需要什么:</p>
<ul>
<li>接口</li>
<li>数据响应成功后在response的哪个字段上面</li>
<li>怎么刷新接口</li>
<li>是否分页,分页初始化</li>
</ul>
<h4>接口</h4>
<p>定义一个api字段,将需要请求的接口传入到组件中,如果有相关参数,需要同时将参数传入到组件中</p>
<h4>数据所在字段</h4>
<p>定义一个resFieldList,比如数据在res.content.data上,则传入数据:</p>
<pre><code>resFieldList: ['content', ‘data’] // 数据所在字段</code></pre>
<p>组件内部则需要在接口请求成功之后做这样一步操作:</p>
<pre><code> let resData = res
const resFieldList = tableInfo.resFieldList
// 得到定义的响应成功的数据字段
for (let i = 0; i < resFieldList.length; i++) {
resData = resData[resFieldList[i]]
}</code></pre>
<p>数据获取成功之后,建议使用父子组件双向通信,.sync或者自定义model都可以实现,将数据派发到父组件,然后由父组件传入子组件渲染组件。 <br>直接由组件内部获取数据并且渲染可能会需要扩展等问题限制组件的使用范围。</p>
<h4>刷新接口</h4>
<p>定义一个refresh字段,刷新页面只需要设置为:</p>
<pre><code>// 刷新表格
tableInfo.refresh = Math.random()</code></pre>
<p>而组件内部watch字段change,重新调获取数据的接口,即可实现刷新功能</p>
<h4>分页相关设置</h4>
<ul>
<li>是否分页,设置字段比如 pager: true/false</li>
<li>是否初始化分页,设置字段比如 initCurpage = Math.random() // 刷新则重置</li>
</ul>
<h2>组件事件处理</h2>
<p>分析有哪几种类型的事件:</p>
<ul>
<li>表头点击事件</li>
<li>列点击事件</li>
<li>表格操作栏点击事件</li>
<li>多选</li>
<li>....</li>
</ul>
<h3>事件中间件的设计</h3>
<p>不同的业务可能涉及到各种类型的事件,如果封装成为了组件,怎么处理???<br>换一个思路,我们把事件看作是一个类型操作,比如点击是click,删除是delete,那我们只需要一个事件转发器,比如:</p>
<pre><code>// 数据渲染事件的派发
this.$emit('handleEvent', 'list', arr)
// 表格选择事件的派发
this.$emit('handleEvent', 'tableCheck', rows)
// 点击事件的派发
this.$emit('handleClick', event, data)</code></pre>
<p>我们定义事件中间件,组件内部发生事件时将事件的类型还有相关的数据派发,父级接收并且处理。</p>
<h2>组件完整字段和使用</h2>
<h3>字段</h3>
<ul>
<li>refresh 刷新数据</li>
<li>api 数据接口</li>
<li>resFieldList 数据成功的响应字段</li>
<li>pager 是否分页</li>
<li>initCurpage 初始化分页</li>
<li>data 表格数据</li>
<li>fieldList 字段列表</li>
<li>handle 操作栏配置</li>
</ul>
<pre><code> // 表格相关
tableInfo: {
refresh: 1,
initCurpage: 1,
data: [],
fieldList: [
{ label: '账号', value: 'account' },
{ label: '用户名', value: 'name' },
{ label: '所属角色', value: 'role_name', minWidth: 120 },
{ label: '性别', value: 'sex', width: 80, list: 'sexList' },
{ label: '账号类型', value: 'type', width: 100, list: 'accountTypeList' },
{ label: '状态', value: 'status', width: 90, type: 'slot', list: 'statusList' },
{ label: '创建人', value: 'create_user_name' },
{ label: '创建时间', value: 'create_time', minWidth: 180 },
{ label: '更新人', value: 'update_user_name' },
{ label: '更新时间', value: 'update_time', minWidth: 180 }
],
handle: {
fixed: 'right',
label: '操作',
width: '280',
btList: [
{ label: '启用', type: 'success', icon: 'el-icon-albb-process', event: 'status', loading: 'statusLoading', show: false, slot: true },
{ label: '编辑', type: '', icon: 'el-icon-edit', event: 'update', show: false },
{ label: '删除', type: 'danger', icon: 'el-icon-delete', event: 'delete', show: false }
]
}
}</code></pre>
<h3>使用</h3>
<pre><code> <!-- 表格 -->
<page-table
:refresh="tableInfo.refresh"
:init-curpage="tableInfo.initCurpage"
:data.sync="tableInfo.data"
:api="getListApi"
:query="filterInfo.query"
:field-list="tableInfo.fieldList"
:list-type-info="listTypeInfo"
:handle="tableInfo.handle"
@handleClick="handleClick"
@handleEvent="handleEvent"
>
<!-- 自定义插槽显示状态 -->
<template v-slot:col-status="scope">
<i
:class="scope.row.status === 1 ? 'el-icon-check' : 'el-icon-close'"
:style="{color: scope.row.status === 1 ? '#67c23a' : '#f56c6c', fontSize: '20px'}"
/>
</template>
<!-- 自定义插槽状态按钮 -->
<template v-slot:bt-status="scope">
<el-button
v-if="scope.data.item.show && (!scope.data.item.ifRender || scope.data.item.ifRender(scope.data.row))"
v-waves
size="mini"
:type="scope.data.row.status - 1 >= 0 ? 'danger' : 'success'"
:icon="scope.data.item.icon"
:disabled="scope.data.item.disabled"
:loading="scope.data.row[scope.data.item.loading]"
@click="handleClick(scope.data.item.event, scope.data.row)"
>
{{ scope.data.row.status - 1 >= 0 ? '停用' : '启用' }}
</el-button>
</template>
</page-table></code></pre>
<h2>最后</h2>
<p><a href="https://link.segmentfault.com/?enc=%2BuCYB03zcW3f5%2BDzUwFRFg%3D%3D.wmf492at8eTxQnPIjml5vDyHZ2YmZDjR0IVnNqmWjgQ%3D" rel="nofollow">演示地址</a></p>
<p><a href="https://link.segmentfault.com/?enc=hr3VXX8FE0q2PN5ORGGWrg%3D%3D.pvOrklT%2F2MC4XF%2F%2FMI75GdQ%2BWiM9hMhnn6oRPK35vHIaMkzhhUDfyKgITMEF7go2" rel="nofollow">github</a></p>
使用mixins,实现elementUI表单的全局验证
https://segmentfault.com/a/1190000018737830
2019-04-02T11:32:55+08:00
2019-04-02T11:32:55+08:00
我在长安长安
https://segmentfault.com/u/wozaichanganchangan
17
<blockquote>使用ElementUi搭建框架的时候,大家应该都有考虑过怎么做全局验证,毕竟复制粘贴什么的是最烦了,这里分享下个人的解决方法。</blockquote>
<h2>验证规则</h2>
<h3>分析规则</h3>
<p>一般验证规则,主要是是否必填,不为空,以及参数类型的验证。<br>基于这个条件,我们开始找找思路, 单个字段的验证是这样的:</p>
<pre><code>name: {
required: 是否必填,
validator: 自定义规则,
message: 失败提示消息(非自定义时触发),
trigger: 触发方式
}</code></pre>
<h3>循环实现</h3>
<p>固定的规则。当一个东西固定之后,那必然是可以重复使用的,并且可以快速生成,我们可以用循环来实现它。<br>但是用循环来实现,我们则需要一个数据规则。</p>
<h4>定义数据规则</h4>
<p>分析下需要的字段,大概就是以下几种,其他的可以根据自身的需求去增加:</p>
<ul>
<li>验证的字段名 label</li>
<li>验证的值 value</li>
<li>验证的类型 type</li>
<li>是否必填 required</li>
<li>自定义规则 rules</li>
</ul>
<p>那最终我们得到的是这样一个字段配置列表:</p>
<pre><code>fieldList: [
{label: '账号', value: 'account', type: 'input', required: true},
{label: '密码', value: 'password', type: 'password', required: true},
{label: '昵称', value: 'name', type: 'input', required: true},
{label: '性别', value: 'sex', type: 'select', list: 'sexList', required: true},
{label: '头像', value: 'avatar', type: 'input'},
{label: '手机号码', value: 'phone', type: 'input'},
{label: '微信', value: 'wechat', type: 'input'},
{label: 'QQ', value: 'qq', type: 'input'},
{label: '邮箱', value: 'email', type: 'input'},
{label: '状态', value: 'status', type: 'select', list: 'statusList', required: true}
]</code></pre>
<p>form完整的字段配置建议参考:</p>
<pre><code> // 表单相关
formInfo: {
ref: null,
data: {
id: '', // *唯一ID
account: '', // *用户账号
password: '', // *用户密码
name: '', // *用户昵称
type: 2, // *用户类型: 0: 手机注册 1: 论坛注册 2: 管理平台添加
sex: 0, // *性别: 0:男 1:女
avatar: '', // 头像
phone: '', // 手机号码
wechat: '', // 微信
qq: '', // qq
email: '', // 邮箱
status: 1 // *状态: 0:停用,1:启用(默认为1)',
// create_user: '', // 创建人
// create_time: '', // 创建时间
// update_user: '', // 修改人
// update_time: '' // 修改时间
},
fieldList: [
{label: '账号', value: 'account', type: 'input', required: true},
{label: '密码', value: 'password', type: 'password', required: true},
{label: '昵称', value: 'name', type: 'input', required: true},
{label: '性别', value: 'sex', type: 'select', list: 'sexList', required: true},
{label: '头像', value: 'avatar', type: 'input'},
{label: '手机号码', value: 'phone', type: 'input'},
{label: '微信', value: 'wechat', type: 'input'},
{label: 'QQ', value: 'qq', type: 'input'},
{label: '邮箱', value: 'email', type: 'input'},
{label: '状态', value: 'status', type: 'select', list: 'statusList', required: true}
],
rules: {},
labelWidth: '120px'
}</code></pre>
<h4>实现验证方法</h4>
<ul>
<li>循环字段列表,根据type判断是提示选择不能为空,还是输入不能为空</li>
<li>如果字段必填,则根据是否有自定义验证去生成验证规则</li>
<li>字段非必填,有自定义规则生成验证</li>
</ul>
<pre><code> // 初始化验证数据
_initRules (formInfo) {
const obj = {},
fieldList = formInfo.fieldList
// 循环字段列表
for (let item of fieldList) {
let type = item.type === 'select' ? '选择' : '输入'
if (item.required) {
if (item.rules) {
obj[item.value] = {
required: item.required,
validator: item.rules,
trigger: 'blur'
}
} else {
obj[item.value] = {
required: item.required,
message: '请' + type + item.label,
trigger: 'blur'
}
}
} else if (item.rules) {
obj[item.value] = {
validator: item.rules,
trigger: 'blur'
}
}
}
formInfo.rules = obj
}</code></pre>
<h2>怎么配置到全局</h2>
<ul>
<li>通过mixin配置,然后在页面中使用(个人使用的是mixin)</li>
<li>配置为全局方法在页面中调用</li>
<li>挂在到vue实例上,通过this即可访问</li>
</ul>
<h2>最后</h2>
<p>在项目的系统管理模块中可以看到示例代码:<br><a href="https://link.segmentfault.com/?enc=8LQRL5kxNXgQKSH01CW4Sg%3D%3D.fcNpuYhIjjcX1ygMAUREL4HXFlNPeGa9ED9gD2rgpkQ%3D" rel="nofollow">项目地址</a><br><a href="https://link.segmentfault.com/?enc=imIF7436gTY3A2ZQ2%2BFvUQ%3D%3D.JVB8LJHRbOH0parB3V2%2Bf5miyj9QfDu1M847V5r3e4j%2Fnfp%2BzPvpaEE9rq4UVK3D" rel="nofollow">项目代码地址</a></p>
数据驱动,快速开发组件(ElementUI篇)
https://segmentfault.com/a/1190000018714612
2019-03-30T21:36:38+08:00
2019-03-30T21:36:38+08:00
我在长安长安
https://segmentfault.com/u/wozaichanganchangan
27
<blockquote>在日常开发中,我们肯定不止一次碰到重复的业务代码,明明功能相似,但总没思路去把它封装成组件。关于封装组件,希望这篇文章能带给大家新的思路,去更高效的完成日常开发。(注:例子都是基于ElementUI, 但思路都是一样的)</blockquote>
<p><a href="https://link.segmentfault.com/?enc=0jLef0rap7ybEvzSrnMWIQ%3D%3D.uBZMpyUoTco6pbz4rFCsUxXx%2BQpRiOMks0BMmqKWz3A%3D" rel="nofollow">示例地址-> </a><a href="https://link.segmentfault.com/?enc=ye0WcerIKVR1W1htl3bGdA%3D%3D.jIsi2ZYj6lSPwEfY%2B7VuVCjbJgLaE4zO7gDWZ9YwtRo%3D" rel="nofollow">https://www.lyh.red/admin</a></p>
<p><a href="https://link.segmentfault.com/?enc=hFUHpeKVOJ9OyJ782HWL4Q%3D%3D.b7qYy8g%2BEhApeutejaPTultAtcemndB7QrhxN3Sm6LNS5ujuRdi3v6PldQF8Jfoh" rel="nofollow">代码地址</a></p>
<h2>数据驱动</h2>
<ul>
<li>构建页面:设计数据结构(绑定value,绑定事件,相关属性)-> 生成dom -> dom绑定相关</li>
<li>监听事件:操作UI -> 触发事件 -> 更新数据 -> 更新UI</li>
</ul>
<p>数据驱动是基于数据触发的,在编写业务的时候,只需要编写好组件的dom结构,之后我们便可以不用再去关心dom层,只需要关心数据就ok。<br>基于这种思路,那留给我们的只有两步,组件设计和数据设计。</p>
<h2>先看看效果</h2>
<blockquote>搜索栏配置以及生成效果</blockquote>
<pre><code> // 过滤相关配置
filterInfo: {
query: {
create_user: '',
account: '',
name: ''
},
list: [
{type: 'input', label: '账户', value: 'account'},
{type: 'input', label: '用户名', value: 'name'},
// {type: 'select', label: '创建人', value: 'create_user'},
// {type: 'date', label: '创建时间', value: 'create_time'},
{type: 'button', label: '搜索', btType: 'primary', icon: 'el-icon-search', event: 'search', show: true},
{type: 'button', label: '添加', btType: 'primary', icon: 'el-icon-plus', event: 'add', show: true}
]
}</code></pre>
<p><img src="/img/bVbqGcE?w=2070&h=126" alt="clipboard.png" title="clipboard.png"></p>
<blockquote>表格配置以及生成效果</blockquote>
<pre><code> // 表格相关
tableInfo: {
refresh: false,
initCurpage: false,
data: [],
fieldList: [
{label: '账号', value: 'account'},
{label: '用户名', value: 'name'},
{label: '性别', value: 'sex', width: 80, list: 'sexList'},
{label: '账号类型', value: 'type', width: 100, list: 'accountTypeList'},
{label: '状态', value: 'status', width: 90, list: 'statusList'},
{label: '创建人', value: 'create_user'},
{label: '创建时间', value: 'create_time', minWidth: 180},
{label: '更新人', value: 'update_user'},
{label: '更新时间', value: 'update_time', minWidth: 180}
],
handle: {
fixed: 'right',
label: '操作',
width: '180',
btList: [
{label: '编辑', type: '', icon: 'el-icon-edit', event: 'update', show: true},
{label: '删除', type: 'danger', icon: 'el-icon-delete', event: 'delete', show: true}
]
}
}</code></pre>
<p><img src="/img/bVbqGcC?w=2070&h=552" alt="clipboard.png" title="clipboard.png"></p>
<blockquote>dom配置和完整页面</blockquote>
<pre><code><template>
<div class="app-container">
<!-- 条件栏 -->
<page-filter
:query.sync="filterInfo.query"
:filterList="filterInfo.list"
:listTypeInfo="listTypeInfo"
@handleClickBt="handleClickBt"
@handleEvent="handleEvent">
</page-filter>
<!-- 表格 -->
<page-table
:refresh="tableInfo.refresh"
:initCurpage="tableInfo.initCurpage"
:data.sync="tableInfo.data"
:api="getListApi"
:query="filterInfo.query"
:fieldList="tableInfo.fieldList"
:listTypeInfo="listTypeInfo"
:handle="tableInfo.handle"
@handleClickBt="handleClickBt"
@handleEvent="handleEvent">
</page-table>
<!-- 弹窗 -->
<page-dialog
:title="dialogInfo.title[dialogInfo.type]"
:visible.sync="dialogInfo.visible"
:width="dialogInfo.width"
:btLoading="dialogInfo.btLoading"
:btList="dialogInfo.btList"
@handleClickBt="handleClickBt"
@handleEvent="handleEvent">
<!-- form -->
<page-form
:refObj.sync="formInfo.ref"
:data="formInfo.data"
:fieldList="formInfo.fieldList"
:rules="formInfo.rules"
:labelWidth="formInfo.labelWidth"
:listTypeInfo="listTypeInfo">
</page-form>
</page-dialog>
</div>
</template></code></pre>
<p><img src="/img/bVbqGdX?w=2556&h=1182" alt="clipboard.png" title="clipboard.png"></p>
<h2>封装一个搜索栏(功能栏)组件</h2>
<h3>根据需求设计数据结构</h3>
<blockquote><strong>参数设计</strong></blockquote>
<p>搜索参数query,比如要查询的参数有账号,名字。</p>
<hr>
<blockquote><strong>dom相关属性设计</strong></blockquote>
<p>首先要考虑dom的类型,和显示,这是基本的,还有扩展类型,比如事件可以设置event属性,是否显示设置show属性,这些是比较通用的。<br>而基于不同类型的dom,如果是<strong>input,select,datetime</strong>类型的dom,作为一个承载数据的容器,则需要一个value属性去和query中的属性名对上,除此之外不同类型的dom还有不同的特定属性,比如select需要提供对应的list,datetime需要相关的pickersOptions去限制时间范围,如果是按钮,比如el-button,则可以设置icon,按钮相关type。</p>
<p>最终实现:</p>
<pre><code>filterInfo: {
query: {
create_user: '',
account: '',
name: ''
},
list: [
{type: 'input', label: '账户', value: 'account'},
{type: 'input', label: '用户名', value: 'name'},
// {type: 'select', label: '创建人', value: 'create_user'},
// {type: 'date', label: '创建时间', value: 'create_time'},
{type: 'button', label: '搜索', btType: 'primary', icon: 'el-icon-search', event: 'search', show: true},
{type: 'button', label: '添加', btType: 'primary', icon: 'el-icon-plus', event: 'add', show: true}
]
}</code></pre>
<p>循环的dom列表</p>
<h3>设计dom结构</h3>
<blockquote><strong>先考虑设计的这个dom需要什么属性</strong></blockquote>
<p>比如dom是el-input,一个输入框,可以设置是否禁止disabled,可以设置是否可清空clearable,v-model要绑定的数据,设置dom的class名,设置dom绑定的事件。<br>比如dom是el-select, 除了上面这些属性,我们还需要这个元素可循环的list</p>
<p>最终dom结构为:</p>
<pre><code> <div class="filter-item" v-for="(item, index) in getConfigList()" :key="index">
<!-- <label class="filter-label" v-if="item.type !== 'button'">{{item.key}}</label> -->
<!-- 输入框 -->
<el-input
:class="`filter-${item.type}`"
v-if="item.type === 'input'"
:type="item.type"
:disabled="item.disabled"
:clearable="item.clearable || true"
:placeholder="getPlaceholder(item)"
@focus="handleEvent(item.event)"
v-model="searchQuery[item.value]">
</el-input>
<!-- 选择框 -->
<el-select
:class="`filter-${item.type}`"
v-if="item.type === 'select'"
v-model="searchQuery[item.value]"
:disabled="item.disabled"
@change="handleEvent(item.even)"
:clearable="item.clearable || true"
:filterable="item.filterable || true"
:placeholder="getPlaceholder(item)">
<el-option v-for="(item ,index) in listTypeInfo[item.list]" :key="index" :label="item.key" :value="item.value"></el-option>
</el-select>
<!-- 时间选择框 -->
<el-time-select
:class="`filter-${item.type}`"
v-if="item.type === 'time'"
v-model="searchQuery[item.value]"
:picker-options="item.TimePickerOptions"
:clearable="item.clearable || true"
:disabled="item.disabled"
:placeholder="getPlaceholder(item)">
</el-time-select>
<!-- 日期选择框 -->
<el-date-picker
:class="`filter-${item.type}`"
v-if="item.type === 'date'"
v-model="searchQuery[item.value]"
:picker-options="item.datePickerOptions || datePickerOptions"
:type="item.dateType"
:clearable="item.clearable || true"
:disabled="item.disabled"
@focus="handleEvent(item.event)"
:placeholder="getPlaceholder(item)">
</el-date-picker>
<!-- 按钮 -->
<el-button
:class="`filter-${item.type}`"
v-else-if="item.type === 'button'"
v-waves
:type="item.btType"
:icon="item.icon"
@click="handleClickBt(item.event)">{{item.label}}</el-button>
</div>
</div></code></pre>
<hr>
<h3>事件的处理</h3>
<blockquote><strong>事件怎么绑定在dom上</strong></blockquote>
<p>绑定事件,可以在数据结构中给dom设置一个event属性,比如说是查询search,在dom结构中我们可以设计一个中间层函数去处理,比如:</p>
<pre><code><!-- 按钮 -->
<el-button
:class="`filter-${item.type}`"
v-else-if="item.type === 'button'"
v-waves
:type="item.btType"
:icon="item.icon"
@click="handleClickBt(item.event)">{{item.label}}</el-button></code></pre>
<p>中间层函数接收事件类型,然后统一处理。</p>
<blockquote><strong>组件中的函数,外部怎么处理</strong></blockquote>
<p>我觉得组件的话,就承载一个去重复的作用,将所以重复的事情去除就可以,像如果是表格,表单,功能栏类似这种可能显示重复但是事件多变性的组件,我们则可以考虑将它们的事件派发到业务相关页面处理,组件保持去除重复的工作,简单干净明了就好了。<br>将事件全部交给父级处理:</p>
<pre><code> // 绑定的相关事件
handleEvent (evnet) {
this.$emit('handleEvent', evnet)
},
// 派发按钮点击事件
handleClickBt (event, data) {
this.$emit('handleClickBt', event, data)
}</code></pre>
<h2>封装一个tree组件</h2>
<blockquote>在后台管理页面树状组件用到次数实在太多了,静态的树数据加载,动态的树数据懒加载,左键点击事件,右键点击事件等等,封装之后,哼哼,谁用谁知道,一个字,爽。</blockquote>
<h3>设计属性</h3>
<pre><code>其实就是将elementui中的大部分用上的tree属性加上,然后再设计一部分让组件更加好用的属性,部分举个例子。</code></pre>
<table>
<thead><tr>
<th align="left">属性</th>
<th align="left">类型</th>
<th align="left">描述</th>
</tr></thead>
<tbody>
<tr>
<td align="left">lazy</td>
<td align="left">Boolean</td>
<td align="left">是否懒加载</td>
</tr>
<tr>
<td align="left">lazyInfo</td>
<td align="left">Array</td>
<td align="left">懒加载相关数据</td>
</tr>
<tr>
<td align="left">loadInfo</td>
<td align="left">Object</td>
<td align="left">正常相关数据</td>
</tr>
<tr>
<td align="left">rightClick</td>
<td align="left">Boolean</td>
<td align="left">是否需要右键菜单</td>
</tr>
<tr>
<td align="left">rightMenuList</td>
<td align="left">Array</td>
<td align="left">右键菜单列表</td>
</tr>
</tbody>
</table>
<blockquote>懒加载数据和正常加载数据结构的详细设计</blockquote>
<pre><code> /**
* 懒加载相关数据
* key -> 唯一标识 label -> 显示 type -> 类型 api -> 接口 params -> 参数 leaf -> 是否叶子节点
*/
lazyInfo: {
type: Array,
default: () => {
return [
{
key: 'id',
label: 'name',
type: '',
api: () => {},
params: {key: '', value: '', type: 'url'}, // url/query->{data: [{key: '', value: '', default: ''}] type: 'query'}
leaf: true
}
]
}
},
/**
* 正常加载相关
*/
loadInfo: {
key: 'id',
label: 'name',
api: () => {},
params: {key: '', value: '', type: 'url'} // url/query->{data: [{key: '', value: '', default: ''}] type: 'query'}
},</code></pre>
<h3>事件处理</h3>
<p>事件处理同样是需要派发到父级处理,以保证组件的可复用性,通过中间件将树组件的相关事件派发搭到父级。</p>
<h3>实现效果</h3>
<p>懒加载树组件相关数据配置:</p>
<pre><code> // 树相关信息
treeInfo: {
refresh: false,
refreshLevel: 0,
nodeKey: 'key',
lazy: true,
type: 0, // 省市区类型
lazyInfo: [
{
key: 'id',
label: 'name',
type: 1,
api: getAllApi,
params: {key: 'pid', value: 1, type: 'url'}
},
{
key: 'id',
label: 'name',
type: 2,
api: getAllApi,
params: {key: 'pid', value: '', type: 'url'},
leaf: true
}
],
rightMenuList: []
},</code></pre>
<p>懒加载树dom结构:</p>
<pre><code> <div class="page-tree" v-loading="treeLoading" @contextmenu.prevent="handleTreeClick">
<el-tree
class="tree-component disabled-select"
ref="TreeComponent"
:show-checkbox="checkBox"
:node-key="nodeKey"
:data="treeData"
:load="handleLoadNode"
:lazy="lazy"
:draggable="draggable"
:allow-drop="handleDrop"
:expand-on-click-node="false"
:check-strictly="checkStrictly"
:filter-node-method="filterNode"
:default-checked-keys="defaultChecked"
:default-expanded-keys="defaultExpanded"
@node-click="handleClickLeft"
@node-contextmenu="handleClickRight"
@check="handleCheck"
@check-change="handleCheck"
@current-change="handleCheck"
@node-expand="handleCheck"
highlight-current
:render-content="renderContent"
:props="treeProps">
</el-tree>
<!-- 右键菜单 -->
<ul class='contextmenu' v-show="rightMenu.show" :style="{left: rightMenu.left +'px',top: rightMenu.top +'px'}">
<li v-for="(item, index) in rightMenu.list" :key="index" @click="handleRightEvent(item.type, item.data, item.node, item.vm)">{{item.name}}</li>
</ul>
</div></code></pre>
<p>实现效果:</p>
<p><img src="/img/bVbqGFH?w=2538&h=1194" alt="clipboard.png" title="clipboard.png"></p>
<h2>总结</h2>
<blockquote>本文以后台管理页面为例,一般后台管理页面常用到的tree, table, form, dialog, 搜索栏已经全部做成了可复用的组件,只需要配置好相关数据,引入组件即可使用。<br>关于组件的相关逻辑,可能要在文章里面一次性说清楚,还是需要费很大的精力,不过希望数据驱动的思想能够让之前没有体会到这种开发乐趣的小伙伴们有到新的想法。</blockquote>
基于vue的验证码组件
https://segmentfault.com/a/1190000017968369
2019-01-21T16:03:39+08:00
2019-01-21T16:03:39+08:00
我在长安长安
https://segmentfault.com/u/wozaichanganchangan
58
<blockquote>最近在自己写页面,模仿思否论坛,然后写登录注册UI的时候需要一个验证码组件. 去搜一下没找到什么合适的,而且大多都是基于后端的,于是自己手写一个。</blockquote>
<h2>演示</h2>
<p><img src="/img/bVbnyrp?w=553&h=405" alt="图片描述" title="图片描述"></p>
<h2>分析验证码组件</h2>
<p><strong>分析验证码功能</strong></p>
<ol>
<li>随机出现的数字大小写字母 (基础功能)</li>
<li>不同的数字或者字母有不同的颜色 (功能优化)</li>
<li>不同的数字或者字母有不同的字体大写 (功能优化)</li>
<li>不同的数字或者字母有不同的边距 (功能优化)</li>
<li>不同的数字或者字母有不同的倾斜角度 (功能优化)</li>
<li>更多功能优化...</li>
</ol>
<hr>
<p><strong>分析组件功能</strong></p>
<ol>
<li>可以设置生成验证码的长度 (基本功能)</li>
<li>可以设置验证码组件的宽高 (基本功能)</li>
</ol>
<hr>
<h2>编写验证码组件</h2>
<p><strong>template</strong></p>
<p>最外层div绑定点击事件,点击后刷新验证码。<br>span是单个验证码的载体,样式动态绑定</p>
<pre><code><template>
<div class="ValidCode disabled-select" :style="`width:${width}; height:${height}`" @click="refreshCode">
<span v-for="(item, index) in codeList" :key="index" :style="getStyle(item)">{{item.code}}</span>
</div>
</template></code></pre>
<p><strong>methods</strong></p>
<p>refreshCode 刷新验证码的方法<br>createdCode 生成验证码的方法<br>getStyle 为每个元素生成动态的样式</p>
<pre><code> methods: {
refreshCode () {
this.createdCode()
},
createdCode () {
let len = this.length,
codeList = [],
chars = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz0123456789',
charsLen = chars.length
// 生成
for (let i = 0; i < len; i++) {
let rgb = [Math.round(Math.random() * 220), Math.round(Math.random() * 240), Math.round(Math.random() * 200)]
codeList.push({
code: chars.charAt(Math.floor(Math.random() * charsLen)), // 随机码
color: `rgb(${rgb})`, // 随机颜色
fontSize: `1${[Math.floor(Math.random() * 10)]}px`, // 随机字号
padding: `${[Math.floor(Math.random() * 10)]}px`, // 随机内边距
transform: `rotate(${Math.floor(Math.random() * 90) - Math.floor(Math.random() * 90)}deg)` // 随机旋转角度
})
}
// 指向
this.codeList = codeList
// 将当前数据派发出去
this.$emit('update:value', codeList.map(item => item.code).join(''))
},
// 动态绑定样式
getStyle (data) {
return `color: ${data.color}; font-size: ${data.fontSize}; padding: ${data.padding}; transform: ${data.transform}`
}
}</code></pre>
<h2><strong>完整代码</strong></h2>
<p><strong>使用</strong></p>
<pre><code><valid-code :value.sync="validCode"></valid-code></code></pre>
<p><strong>组件</strong></p>
<pre><code><template>
<div class="ValidCode disabled-select" :style="`width:${width}; height:${height}`" @click="refreshCode">
<span v-for="(item, index) in codeList" :key="index" :style="getStyle(item)">{{item.code}}</span>
</div>
</template>
<script>
export default {
name: 'validCode',
props: {
width: {
type: String,
default: '100px'
},
height: {
type: String,
default: '40px'
},
length: {
type: Number,
default: 4
}
},
data () {
return {
codeList: []
}
},
mounted () {
this.createdCode()
},
methods: {
refreshCode () {
this.createdCode()
},
createdCode () {
let len = this.length,
codeList = [],
chars = 'ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz0123456789',
charsLen = chars.length
// 生成
for (let i = 0; i < len; i++) {
let rgb = [Math.round(Math.random() * 220), Math.round(Math.random() * 240), Math.round(Math.random() * 200)]
codeList.push({
code: chars.charAt(Math.floor(Math.random() * charsLen)),
color: `rgb(${rgb})`,
fontSize: `1${[Math.floor(Math.random() * 10)]}px`,
padding: `${[Math.floor(Math.random() * 10)]}px`,
transform: `rotate(${Math.floor(Math.random() * 90) - Math.floor(Math.random() * 90)}deg)`
})
}
// 指向
this.codeList = codeList
// 将当前数据派发出去
this.$emit('update:value', codeList.map(item => item.code).join(''))
},
getStyle (data) {
return `color: ${data.color}; font-size: ${data.fontSize}; padding: ${data.padding}; transform: ${data.transform}`
}
}
}
</script>
<style scoped lang="scss">
.ValidCode{
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
span{
display: inline-block;
}
}
</style>
</code></pre>
<p><a href="https://link.segmentfault.com/?enc=vt4UmIW2LbGaf6ZVwgMdsg%3D%3D.8Nn8xH3GOHy3t7heVDDZ%2FN1x0csdmguh2S%2FNXpK9r3KS%2F9N%2FWEfZctYWaP7Xmy1wpXOY%2FZSi1VGjc7GxUWdivEICovOXK%2Bm48Em5D4VyQWY%3D" rel="nofollow">源码地址</a><br><a href="https://link.segmentfault.com/?enc=X2pNUunW8Y701qWWT50YdQ%3D%3D.MgBRUZ7ZzkOQLbs3z0U1IrBV271HNJlqS33VVDdaa2w%3D" rel="nofollow">演示地址</a> 模仿思否写的网站,点注册可看到验证码</p>
不能使用for循环,传入n和m, 生成一个长度为n,每一项都是m的数组
https://segmentfault.com/a/1190000017876659
2019-01-14T00:33:37+08:00
2019-01-14T00:33:37+08:00
我在长安长安
https://segmentfault.com/u/wozaichanganchangan
19
<blockquote>逛知乎的时候看到一个问题,不能使用for循环,传入n和m, 生成一个长度为n,每一项都是m的数组。第一反应是递归,然后再想到正则,还在思考中...</blockquote>
<h2>递归法</h2>
<pre><code>function getArr(n,m) {
// 方法一: 递归
let arr = new Array(n)
function setData (index) {
if (index >= 0) {
if (!arr[index]) {
arr[index] = m
}
setData(index - 1)
}
}
setData(arr.length - 1)
return arr
}
getArr(10, 'aa') // ["aa", "aa", "aa", "aa", "aa", "aa", "aa", "aa", "aa", "aa"]</code></pre>
<h2>正则法</h2>
<pre><code>function getArr(n,m) {
// 方法二: 正则 (缺点:当传入的数据带,时,以,为分隔符会出错,逻辑还需要优化)
let str = new Array(n).join(' ') // 生成对应长度的字符串
str = str.replace(/\s?/g, m) // 使用正则替换得到对应的字符串
str = str.replace(new RegExp(`(${m})`, 'g'), '$1,') // 得到逗号分隔的字符串
str = str.substring(0, str.length - 1)
return str.split(',')
}
getArr(3, '12345') // ["12345", "12345", "12345"]</code></pre>
<h2>fill()</h2>
<ul><li>来自<a href="https://segmentfault.com/u/ntnyq">ntnyq</a>, <a href="https://segmentfault.com/u/ntnyq">ntnyq</a>说的数组的fill()方法,之前都不知道的,感谢,然后立刻就想到了类似可以去完成功能的,map()和filter()。</li></ul>
<pre><code>function getArr(n,m) {
// 方法三: fill()
return Array(n).fill(m)
}
getArr(10, 'aa') // ["aa", "aa", "aa", "aa", "aa", "aa", "aa", "aa", "aa", "aa"]</code></pre>
<h2>map()</h2>
<ul><li>来自<a href="https://segmentfault.com/u/zaijiekedelieren">在捷克的猎人</a>(我后面也想到了,哈哈)</li></ul>
<pre><code>function getArray(n, m){
return result = Array(n).join(",").split(",").map(() => {
return m
})
}</code></pre>
<h2>while</h2>
<ul><li>可以用while 或 do ... while,来自<a href="https://segmentfault.com/u/diary">灰色v碰触</a>
</li></ul>
<p>用while用的少,一下就想不起来了...</p>
<hr>
<p><strong>希望能看到大家更好的方法,感觉自己进了死胡同...</strong></p>
深入浅出,手把手教你编写正则表达式
https://segmentfault.com/a/1190000017862694
2019-01-11T23:24:11+08:00
2019-01-11T23:24:11+08:00
我在长安长安
https://segmentfault.com/u/wozaichanganchangan
30
<blockquote>日常代码的开发中,大家都或多或少的碰到一些正则表达式,但有的朋友只是会用,或者大致明白,希望这篇对正则深入浅出的文章能够让大家有所收获。</blockquote>
<h2>基本语法</h2>
<pre><code>[xyz] 一个字符集,匹配任意一个包含的字符
[^xyz] 一个否定字符集,匹配任何为包含的字符
\w (小写) 匹配字母或数字或者下划线的字符
\W (大写) 匹配不是字母,数字,下划线的字符
\s (小写) 匹配任意空白符
\S (大写) 匹配不是空白符的字符
\d (小写) 匹配数字
\D (大写) 匹配非数字的字符
\b (小写) 匹配单词的开始或结束的位置
\B (大写) 匹配不是单词开头或结束的位置
$ 匹配字符串的结束
^ 匹配字符串的开始
. 匹配所有,除了换行符
- 重复0次或更多次
- 重复1次或更多次
? 重复0次或一次
{n} 重复n次
{n,} 重复n次或更多次
{n, m} 重复n次到m次
等更多.........</code></pre>
<p>更多语法可以在<a href="https://link.segmentfault.com/?enc=E2POiy2pRoqT9wdt8pKqfw%3D%3D.SiYr%2FInKyTC0sjSJ47FAa3syvKIdWwwrfmkiPURUHa4U%2BrADODSJvuLBZ3uodt2vhvXEb2qkMD8j4EX6aJ8PQg%3D%3D" rel="nofollow">W3C正则</a>里查看</p>
<h2>匹配位置</h2>
<p><strong>需要强调一下匹配位置的几个语法, 后面会一一举例说明</strong><br>^ 匹配字符串的开始<br>$ 匹配字符串的结尾<br>(?=pattern) 正向前瞻,字符串匹配满足条件的位置<br>(?!pattern) 负向前瞻,字符串匹配满足条件的位置</p>
<h2>常用方法和属性</h2>
<hr>
<p>正则表达式的写法有两种</p>
<pre><code>var reg = new RegExp('/1/')
var reg = /1/</code></pre>
<p>reg是正则对象的实例,通过console.dir打印对象,我们能看到实例上面的属性和方法。</p>
<p><img src="/img/bVbm7xc?w=601&h=657" alt="clipboard.png" title="clipboard.png"></p>
<ul><li>常用的方法</li></ul>
<p>举个简单的例子,我们来了解下test()和exec()的区别和使用场景。</p>
<pre><code>var reg = /1/, reg1 = /(1)/
reg.test(1111) // true
reg.test(222) // false
reg.exec(11112) // ["1", index: 0, input: "11112"]
reg1.exec(11112) // ['1111', '1', index: 0, input: '11112']
reg1.exec(222) // null</code></pre>
<p>test验证后会返回一个布尔值,主要用于验证是否匹配,exec则在验证成功后返回一个类似数组的对象,主要用于捕获分组,失败则返回null。</p>
<p>reg1是一个匹配(1)分组的正则,使用exec匹配成功后返回['1111', '1', index: 0, input: '11112'],input是输入的值,第一项是匹配满足条件的数据,第二项是匹配到的分组,如果没有分组,第二项则不存在,index属性则表示从第几项开始匹配到。<br>关于分组的话在下面会有详细的例子说明。</p>
<ul><li>常用的属性</li></ul>
<pre><code> ignoreCase 忽略大小写,默认为false
global 全局匹配,默认为false
multiline 在有换行的时候,可以得到换行的起始位置和终止位置,默认为false</code></pre>
<h2>匹配规则</h2>
<p><em>正则表达式(regular expression)描述了一种字符串匹配的模式,可以用来检查一个串是否含有某种子串、将匹配的子串做替换或者从某个串中取出符合某个条件的子串等</em></p>
<blockquote>当当看描述和说明文档可能有点太官方化了,先假设我们已经都了解了正则匹配的基本语法和大致的使用,接下来这里用简浅的例子一步一步来分析怎样去实现自己需要的正则表达式。</blockquote>
<ul><li>例子: 手机号码</li></ul>
<p>首先举个例子,要匹配一个手机号码。先分析一下手机号码有什么样的规则呢,1开头,必须为数字,而且总共长度为十一位,那我们就有了下面这个表达式:</p>
<pre><code>var reg = /^1[0-9]{10}$/ // 根据正则语法 ^为匹配开始,^1就表示第一位必须为1开头,[0-9]指的是必须为0-9的数字,{10}表示重复次数为10,$结束符。简单的匹配规则完成。</code></pre>
<p>好像有点粗糙,一般手机号码第二位是不会有0-9这么多的可能的,所以我们需要在优化一下,第一位为1,第二位为[3,5,8,7]:</p>
<pre><code>var reg = /^1[3,5,8,7]{1}[0-9]{9}$/ // 根据正则语法 ^为匹配开始,^1就表示第一位必须为1开头,[3,5,8,7]指的是匹配其中任一数字,{1}表示重复次数为1,前两位匹配完成,加上后面的[0-9],{9}重复九次,$结束符。简单的匹配规则完成。</code></pre>
<p>这样,基本的一个手机号码的正则匹配规则就出来了,如果自己业务有需求,做相对应的改动就可以了。</p>
<ul><li>例子:邮箱</li></ul>
<p>和手机号码的规则相比较,邮箱的匹配规则稍微复杂一点。同样,我们先分析邮箱的规则,比如QQ邮箱,110@qq.com,先有字符,长度大于1,然后是@,后面再跟着一串字符长度大于1,那我们就可以得到这样一个的表达式:</p>
<pre><code>var reg = /^[\w\.]+@[\w.]+$/ // [\w\.] \w匹配字母或数字或者下划线的字符,\.通过转义符表示匹配.,+表示重复一次或更多次,匹配@符号,然后又是同样的\w\.匹配一次或更多次。</code></pre>
<ul><li>例子:网站</li></ul>
<p>同样是分析网站规则,http(s)://segmentfault.com/1233, 首先可能是http协议或者https协议,然后是://,然后是字符串。这样一分析,再把对应的正则语法理一理,很快一个匹配规则就出来了:</p>
<pre><code>var reg = /^https?:\/\/.+$/ // 首先会是http开头,^http, 然后s跟随?表示匹配0次或一次,:,//需要用\转义,后面跟随各种类型的不确定因素.+,当然,如果我们要做的更精确的匹配,则可以修改成自己的规则即可。</code></pre>
<p>举了几个基本的例子,让我们去复习巩固正则的基本使用和匹配规则,首先分析要匹配的规则是什么,然后一步一步去把规则拼积木一样拼进去,就可以了,自己多定义一些不一样的规则然后去一个一个实现,就会发现如果只是基本的使用,远没有想象中的难。</p>
<h2>分组</h2>
<p>分组的就是把要匹配的规则分成一个组,写在()里,比如匹配数字(0-9),匹配字母(0-z),主要有分为捕获型分组和非捕获型分组。</p>
<ul><li>捕获型分组</li></ul>
<p>先来说说捕获型分组。主要可以干的事情有两个,引用和反向引用,在一些稍微复杂的正则表达式里,我们常常会用到这些。</p>
<ul><li>捕获型分组-引用</li></ul>
<p>在每次分组捕获之后,RegExp对象上面可以拿到最近捕获到的分组,下面来举一些例子:</p>
<pre><code>var reg = /(1)(3)/ // 两个分组
reg.exec(123134) // 捕获到了两个分组
console.log(RegExp.$1) // 1
console.log(RegExp.$2) // 3</code></pre>
<p>得到到了最近捕获的分组,可是往往我们不明白捕获到了分组有什么用,下面举个例子,将所有的html标签替换成p标签,来说明捕获到的分组怎么去引用。<br>首先我们分析一下这个匹配的规则,开始标签为<字符串>,结束标签为</字符串>,我们只需要把这些替换为<P> </p> 就可以了。</p>
<pre><code>var str = '<span>1234</span><div>456</div>'
var reg = /<(\/?)(\w+)>/g // 先匹配<,然后(/?)重复0或一次,然后匹配\w+,匹配>,结束。
str.replace(reg, '<$1p>') // 字符串的替换方法,这里使用了$1,就是RegExp.$1,引用正则里面的分组(\/?),
// 所以<$1p>中的$1则为动态捕获到的分组。当标签为</元素>时,RegExp.$1为/,
// 字符串中用</p>替换原来标签,
// 则达到了将html字符串中开始标签和闭合标签全部替换的效果</code></pre>
<ul><li>捕获型分组-反向引用</li></ul>
<p>反向引用也是一样将捕获到的分组引用,不过是在编写的正则里面,通过/1,/2,/3可以得到当前捕获到的分组.</p>
<ul><li>非捕获型分组</li></ul>
<p>(?:)即表示该分组不能被捕获,之后在引用的时候是引用不到这个分组的。</p>
<h2>贪婪匹配和惰性匹配</h2>
<h2>正向前瞻和负向前瞻</h2>
从0开始,编写一个验证函数(工具函数)
https://segmentfault.com/a/1190000017528129
2018-12-26T23:06:50+08:00
2018-12-26T23:06:50+08:00
我在长安长安
https://segmentfault.com/u/wozaichanganchangan
4
<blockquote><ul>
<li>为什么要写验证函数</li>
<li>将验证过程变成多个步骤</li>
<li>完成一个基本的验证函数</li>
</ul></blockquote>
<hr>
<h2>1. 为什么要写验证函数</h2>
<p>之前写项目的时候,一般都是在登录注册,修改密码的时候有需要些正则的需求,所以每次用到的时候都是直接从前面的代码copy过去就好了,直到后台开始写后台管理系统类的项目,复制粘贴已经完全不可行了。怎么能做一个只会ctrl+c,ctrl+v的程序猿呢!简直不能忍,于是就想到了自己写一个验证函数,每次需要做验证的时候只需要调用这个函数,传入参数就可以了,想想都美滋滋。</p>
<hr>
<h2>2. 将验证过程变成多个步骤</h2>
<hr>
<p>做验证的时候我们要做的,定义验证失败后的提示,编写验证的方法,然后调用验证得到结果。<br>一个验证的过程,一般便是分为这几步,我们可以按照这个步骤设计出自己的验证函数。</p>
<pre><code>var validatorObj = {
// 验证定义
validator: {
// 验证失败后的提示
messages: {},
// 验证的方法, 返回一个布尔值
methods: {}
},
// 得到验证结果
checkResult: {}
}</code></pre>
<h3>定义一些验证失败的提示</h3>
<p><strong>定义的错误提示可以自定义,至于{0} {1}等则是用来做一个标识符,在验证失败后会将要验证的参数替换掉标识符</strong></p>
<pre><code>// 验证失败后的提示
messages: {
notnull: '请输入{0}',
max: '长度最多为 {1} 个字符',
min: '长度最小为 {1} 个字符',
length: '{0}的长度在 {1} 到 {2} 个字符',
number: '{0}必须是数字',
string: '{0}必须是字母或者数字',
moblie: '{0}必须是手机或者电话号码格式',
noChinese: '{0}不能为中文',
lon: '{0}范围为-180.0~+180.0(必须输入1到10位小数)',
lat: '{0}范围为-90.0~+90.0(必须输入1到10位小数)',
url: '请输入正确的{0}访问地址',
repeat: '两次输入的{0}不一致',
email: '邮箱格式不正确',
password: '请输入由大小写字母+数字组成的6-16位密码',
fixedNum: '请输入{1}位数字'
}</code></pre>
<h3>定义对应的验证方法</h3>
<p><strong>可以看到几乎在每个验证前面都加了一个当数据为空的时候,返回为true,这是因为有的时候我们并不关心某一个数据是否填写,但一旦填写了,又要求符合某种规则。所以如果要验证非空的时候,需要使用两个验证属性。</strong></p>
<pre><code>
// 验证的方法, 返回一个布尔值
methods: {
notnull: obj => {
return obj.value || obj.value === 0
},
max: obj => {
if (!obj.value) return true
return obj.conditions[0] >= obj.value.length
},
min: obj => {
if (!obj.value) return true
return obj.value.length >= obj.conditions[0]
},
length: obj => {
if (!obj.value) return true
return obj.conditions[0] <= obj.value.length && obj.value.length <= obj.conditions[1]
},
number: obj => {
if (!obj.value) return true
reg = /^[0-9]+.?[0-9]*$/
return reg.test(obj.value)
},
string: obj => {
if (!obj.value) return true
reg = /^[a-zA-Z0-9]+$/
return reg.test(obj.value)
},
moblie: obj => {
if (!obj.value) return true
reg = /^(1[3,5,8,7]{1}[\d]{9})|(((400)-(\d{3})-(\d{4}))|^((\d{7,8})|(\d{4}|\d{3})-(\d{7,8})|(\d{4}|\d{3})-(\d{3,7,8})-(\d{4}|\d{3}|\d{2}|\d{1})|(\d{7,8})-(\d{4}|\d{3}|\d{2}|\d{1}))$)$/
return reg.test(obj.value)
},
noChinese: obj => {
if (!obj.value) return true
reg = /[\u4e00-\u9fa5]/
return !reg.test(obj.value)
},
lon: obj => {
if (!obj.value) return true
reg = /^[\-\+]?(0?\d{1,2}\.\d{1,5}|1[0-7]?\d{1}\.\d{1,10}|180\.0{1,10})$/
return reg.test(obj.value)
},
lat: obj => {
if (!obj.value) return true
reg = /^[\-\+]?([0-8]?\d{1}\.\d{1,10}|90\.0{1,10})$/
return reg.test(obj.value)
},
url: obj => {
if (!obj.value) return true
reg = /^([hH][tT]{2}[pP]:\/\/|[hH][tT]{2}[pP][sS]:\/\/)(([A-Za-z0-9-~]+)\.)+([A-Za-z0-9-~\/])+$/
return reg.test(obj.value)
},
repeat: obj => {
if (!obj.value) return true
return obj.value === obj.value1
},
email: obj => {
if (!obj.value) return true
reg = /^([-_A-Za-z0-9\.]+)@([_A-Za-z0-9]+\.)+[A-Za-z0-9]{2,3}$/
return reg.test(obj.value)
},
password: obj => {
if (!obj.value) return true
reg = /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])[a-zA-Z\d]{6,16}$/
return reg.test(obj.value)
},
fixedNum: obj => {
if (!obj.value) return true
return obj.value.length === obj.conditions[0]
}
}</code></pre>
<h3>调用验证方法</h3>
<p>**这里是调用验证函数的方法,和上面的定义结合起来。<br>传入要验证的规则,验证的值,验证的字段名字,如果有条件则加上条件数组(条件数组是需要我们自己去设计的)**</p>
<pre><code>/**
1. 传入验证规则,得到验证结果
2. @param {Obj} { label, value, rules, conditions}
3. @param {String} label: 验证的字段名称
4. @param {String} value: 验证的值 (验证重复的时候可以添加value1属性)
5. @param {Array} rules: 验证的规则数组 例如: ['notnull', 'length'] 如果参数必填,第一个参数为notnull
6. @param {Array} conditions: 条件字段 例如: ['2', '10'] ,则验证长度错误会提示: 密码的长度在2到10个字符,以传入数组的条件去做验证, 验证的提示{1}开始将匹配的是当前数组
7. @return {obj} { result, message } 验证结果对象
*/
// 得到验证结果
checkResult: function (obj) {
let result = true,
checkType,
message = '验证成功',
validatorMethods = this.validator.methods,
validatorMessage = this.validator.messages
// 循环验证
for (let i = 0, len = obj.rules.length; i < len; i++) {
// 当验证的规则不存在,默认跳过这个验证
if (!validatorMethods[obj.rules[i]]) {
console.log(obj.rules[i] + '规则不存在')
break
}
// 得到当前验证失败信息
if (!validatorMethods[obj.rules[i]](obj)) {
checkType = obj.rules[i]
result = false
break
}
}
// 如果验证失败, 得到验证失败的结果集
if (!result) {
message = validatorMessage[checkType]
if (obj.conditions) {
obj.conditions.forEach((item, index) => {
message = message.replace('{' + (index + 1) + '}', item)
})
}
message = message.replace('{0}', obj.label)
return {result, message}
}
return {result, message}
}</code></pre>
<h2>3. 完整的验证函数</h2>
<p>把上面的步骤拼在一起,就可以完成一个验证函数。具体的需求和使用,可以根据项目自定义,但思路大致是这样的。</p>
<pre><code>/**
* 传入验证规则,得到验证结果
* @param {Obj} { label, value, rules, conditions}
* @param {String} label: 验证的字段名称
* @param {String} value: 验证的值 (验证重复的时候可以添加value1属性)
* @param {Array} rules: 验证的规则数组 例如: ['notnull', 'length'] 如果参数必填,第一个参数为notnull
* @param {Array} conditions: 条件字段 例如: ['2', '10'] ,则验证长度错误会提示: 密码的长度在2到10个字符,以传入数组的条件去做验证, 验证的提示{1}开始将匹配的是当前数组
* @return {obj} { result, message } 验证结果对象
*/
function validate (obj) {
let reg
const validatorObj = {
// 验证定义
validator: {
// 验证失败后的提示
messages: {
notnull: '请输入{0}',
max: '长度最多为 {1} 个字符',
min: '长度最小为 {1} 个字符',
length: '{0}的长度在 {1} 到 {2} 个字符',
number: '{0}必须是数字',
string: '{0}必须是字母或者数字',
moblie: '{0}必须是手机或者电话号码格式',
noChinese: '{0}不能为中文',
lon: '{0}范围为-180.0~+180.0(必须输入1到10位小数)',
lat: '{0}范围为-90.0~+90.0(必须输入1到10位小数)',
url: '请输入正确的{0}访问地址',
repeat: '两次输入的{0}不一致',
email: '邮箱格式不正确',
password: '请输入由大小写字母+数字组成的6-16位密码',
fixedNum: '请输入{1}位数字'
},
// 验证的方法, 返回一个布尔值
methods: {
notnull: obj => {
return obj.value || obj.value === 0
},
max: obj => {
if (!obj.value) return true
return obj.conditions[0] >= obj.value.length
},
min: obj => {
if (!obj.value) return true
return obj.value.length >= obj.conditions[0]
},
length: obj => {
if (!obj.value) return true
return obj.conditions[0] <= obj.value.length && obj.value.length <= obj.conditions[1]
},
number: obj => {
if (!obj.value) return true
reg = /^[0-9]+.?[0-9]*$/
return reg.test(obj.value)
},
string: obj => {
if (!obj.value) return true
reg = /^[a-zA-Z0-9]+$/
return reg.test(obj.value)
},
moblie: obj => {
if (!obj.value) return true
reg = /^(1[3,5,8,7]{1}[\d]{9})|(((400)-(\d{3})-(\d{4}))|^((\d{7,8})|(\d{4}|\d{3})-(\d{7,8})|(\d{4}|\d{3})-(\d{3,7,8})-(\d{4}|\d{3}|\d{2}|\d{1})|(\d{7,8})-(\d{4}|\d{3}|\d{2}|\d{1}))$)$/
return reg.test(obj.value)
},
noChinese: obj => {
if (!obj.value) return true
reg = /[\u4e00-\u9fa5]/
return !reg.test(obj.value)
},
lon: obj => {
if (!obj.value) return true
reg = /^[\-\+]?(0?\d{1,2}\.\d{1,5}|1[0-7]?\d{1}\.\d{1,10}|180\.0{1,10})$/
return reg.test(obj.value)
},
lat: obj => {
if (!obj.value) return true
reg = /^[\-\+]?([0-8]?\d{1}\.\d{1,10}|90\.0{1,10})$/
return reg.test(obj.value)
},
url: obj => {
if (!obj.value) return true
reg = /^([hH][tT]{2}[pP]:\/\/|[hH][tT]{2}[pP][sS]:\/\/)(([A-Za-z0-9-~]+)\.)+([A-Za-z0-9-~\/])+$/
return reg.test(obj.value)
},
repeat: obj => {
if (!obj.value) return true
return obj.value === obj.value1
},
email: obj => {
if (!obj.value) return true
reg = /^([-_A-Za-z0-9\.]+)@([_A-Za-z0-9]+\.)+[A-Za-z0-9]{2,3}$/
return reg.test(obj.value)
},
password: obj => {
if (!obj.value) return true
reg = /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])[a-zA-Z\d]{6,16}$/
return reg.test(obj.value)
},
fixedNum: obj => {
if (!obj.value) return true
return obj.value.length === obj.conditions[0]
}
}
},
// 得到验证结果
checkResult: function (obj) {
let result = true,
checkType,
message = '验证成功',
validatorMethods = this.validator.methods,
validatorMessage = this.validator.messages
// 循环验证
for (let i = 0, len = obj.rules.length; i < len; i++) {
// 得到当前验证失败信息
if (!validatorMethods[obj.rules[i]](obj)) {
checkType = obj.rules[i]
result = false
break
}
}
// 如果验证失败, 得到验证失败的结果集
if (!result) {
message = validatorMessage[checkType]
if (obj.conditions) {
obj.conditions.forEach((item, index) => {
message = message.replace('{' + (index + 1) + '}', item)
})
}
message = message.replace('{0}', obj.label)
return {result, message}
}
return {result, message}
}
}
return validatorObj.checkResult(obj)
}
export default validate
</code></pre>
<h3>使用示例</h3>
<pre><code>validate({label: 'username', value: 'admin', rules: ['notnull', 'length'], conditions: ['2', '10']}) // 验证username不为空且长度在2-10之间
validate({label: 'pawwword', value: 'lllyh111', rules: ['notnull', 'password']}) // 验证password由大小写字母+数字组成的6-16位密码</code></pre>
<p>验证返回结果大概长这样:</p>
<pre><code>{result: true, message: '验证成功'}
{result: false, message: '验证失败提示'}</code></pre>
<h2>4.在页面上的使用</h2>
<p>把函数放在全局,需要做验证的地方直接调用这个函数就ojbk了。</p>
<h3>在Elementui中的例子</h3>
<pre><code> // 检测号码
const checkMobile = (rule, value, callback) => {
let check = this.$validate({label: '号码', value, rules: ['moblie']})
if (!check.result) {
callback(new Error(check.message))
} else {
callback()
}
}
// 检测非中文
const checkWechat = (rule, value, callback) => {
let check = this.$validate({label: '微信', value, rules: ['noChinese', 'max'], conditions: [12]})
if (!check.result) {
callback(new Error(check.message))
} else {
callback()
}
}</code></pre>
<p><a href="https://link.segmentfault.com/?enc=60sP4KEo4MmqVwfAaybKaA%3D%3D.tLgrMp97CP9jd5UKmDHmb1sqEj9CyX24pDuB1ul5pDSxY8nx7zSDjTOGKZXyl6GQK1%2FEWotBLt7OTF0qOSSPx0paXVY8bKP6zwhv9ilCgNs%3D" rel="nofollow">源码---如何使用</a><br><a href="https://link.segmentfault.com/?enc=HW41lv1SN2yYgTKnAb8UUw%3D%3D.%2BTueinDRZR5ZVbPoe9r8z2SlM9ZGdqC6ewsWBI3oSZk%3D" rel="nofollow">登录-系统管理-用户管理中可看到相关验证效果</a></p>