记录实习一个月期间的任务、碰到的问题及解决方法
第一周(7.9-7.13)
任务描述
基于 vue.js + element ui 实现一个分级选择器(dropdown list)组件,要求同时支持单选击中、输入关键词筛选后单击选中和多选等方式。原始数据为 json 格式的部门信息,包括多个字段,其中 orgCode
字段为每个部门的唯一标志,也是作为级联选择器中的 key 值。
用到的框架
- vue.js
- element UI ( Dialog, Casader, Button, Tag )
设计实现
1. 级联选择器 el-cascader
点击 input 框触发,用于选择所需要的部门,每次只能选一个,可通过多次点击来完成多选操作,配置参数如下:
- 支持搜索 :
filterable
- 鼠标悬浮触发展开子列表 :
expand-trigger="hover"
- 输入防抖 :
debounce=200
- 默认关闭所有子列表 :
show-all-levels="false"
- 数据选项为按照级联列表数据格式处理后的 json 数据 :
options="formatDataList"
- 选中内容发生改变之后触发添加选项函数 :
@change="addOption"
2. 标签展示器
一个标签展示框用于展示所有选中的部门选项,每个选中的选项用一个可删除的标签显示,可点击标签右上角的关闭符号删除此选项,则该选项也会从选中的数据列表中移出。
3. 重复选择对话框
一个弹出的 Dialog 对话框,用于防止重复选择相同的选项, 通过属性 alertDialogVisible
控制它的显示和隐藏。
主要工作内容
- 使用 vue-cli 搭建一个这样的 component;
- 数据的预处理,将原始的 json 数据转换成满足 cascader 格式要求的 json 数据;
- 编写相应的事件处理函数:
getItemChildren
: 数据预处理时用于获取某个 item 的子节点,返回一个 children 数组;addOption
: 级联列表选中内容发生变化时的相应函数,用于添加选项到已选中列表中;getSelectedItems
: 获取已选中的列表,需要将 cascader 格式的数据转换成原始的数据格式;handleClose
: 标签关闭的响应函数,需要从已选中列表中删除对应的选项;
遇到的问题(需求)及解决方法
1. Cascader 选中之后会默认关闭级联选择框,每次选择都要重新打开?
针对这个问题,这是 Cascader 原本的特性决定的,查看了它的源码之后发现,它的选中某个选项的处理函数如下:
1
2
3
4
5
6
7
8
9
10
11 > handlePick(value, close = true) {
> this.currentValue = value;
> this.$emit('input', value);
> this.$emit('change', value);
> if (close) {
> this.menuVisible = false;
> } else {
> this.$nextTick(this.updatePopper);
> }
> }
>
它其实是有一个 close 参数可以选择是否关闭级联列表的,但是它这个组件本身只提供了一个 change 事件可以监听到选中数据的变化,通过断点调试发现,change 事件的响应函数触发的时候 handlePick 函数已经执行完成了没办法通过这个函数手动设置 close 参数为 false。
既然不能阻止它关闭,只能退而求其次在它关闭之后再次打开,实现一种“伪连续”的状态。这里通过在 change 事件的响应函数 addOption 中完成,实现如下:
1
2
3
4 > this.$nextTick(function() {
> vm.$refs.customedCascader.handleClick();
> });
>
在完成选中列表更新之后设置,浏览器下一帧将重新触发 Cascader 的 handleClick 函数,相当于自动点击了一次 input 框,这个函数里面将会重新打开级联列表。
这种做法虽然勉强能实现“伪连续选择”的功能,但是每次还是能看到轻微的跳闪,效果也不是很好。
2. Cascader 通过筛选匹配的关键字加粗效果不是很明显,改成彩色更好;选中之后输入的关键字被清空,无法停留在选择前的子菜单。
颜色问题可以通过手动设置对应标签的全局样式来覆盖 Cascader 内置的样式来实现(会影响到所有包含此标签的元素,谨慎使用)。
停留在选中前的子菜单暂时没办法实现,受 Cascader 组件本身特性的限制,它暴露的接口有限,由于本身连续选择的功能就是上一步手动实现的一个“伪功能”,再加上子菜单是又通过 hover 操作触发的,所以没办法再重新打开之后再定位到原本选中的子菜单。
输入框被清空的问题也可以通过记录下输入的关键字,然后再在重新打开之后手动填充回去,如果需要显示筛选后的结果的话,也可以通过手动调用 Cascader 的 handleInputChange(value)函数来实现。
第二周(7.16-7.20)
任务描述
跟上周的任务一样,基于 vue.js + element ui 实现一个分级选择器(dropdown list)组件,要求同时支持单选击中、输入关键词筛选后单击选中和多选等方式。原始数据为 json 格式的部门信息,包括多个字段,其中 orgCode
字段为每个部门的唯一标志,也是作为级联选择器中的 key 值。
用到的框架
- vue.js
- element UI ( Dialog, Tree, Button, Input, Popover )
- clickoutside.js
主要实现要点、遇到的问题及解决方法
1. 数据预处理
直接可复用上一周的函数。
2. 带输入框的树形结构默认是都显示的,改成 popOver 触发才显示之后,click/hover/focus 等触发方式都有局限,如何让 popOver 框里的树形结构只在点击输入框和树形结构之外的地方才隐藏?
手动触发+改造 clickoutside;通过两个状态属性间接控制 popOver 框的显示和隐藏。
3. 匹配的关键字高亮
暂时无法实现,popOver 没有像 cascader 那样的匹配机制,cascader 会在匹配之后将匹配的文字单独放在一个标签里面加粗显示,但是 popOver 没有这样的机制,所有没办法单独高亮显示匹配到的关键字。
4. 提交按钮在输入框下方导致 popOver 弹出框会将提交按钮挡住
设置 popOver 弹出框和提交 button 都为 inline-block,放置在同一行
5.控制是否只返回选中的叶节点还是所有中间节点也要返回
原始数据本身自带有“isLeaf”属性,可用于判断是否为叶节点。
6. 实现了一个 dialog 展示已选中的选项,让用户可以确认一下,点击确认之后提交数据到相应的 api 接口,同时需要清空已选中的节点信息并且将所有子节点初始化为重叠状态。
调用 Tree.setCheckedNodes([])和 Tree.store._getAllNodes()[i].expanded=false 实现清空操作。
第三周(7.23-7.27)
任务描述
基于上周的 vue 下拉菜单组件,搭建一个简单的 python-flask 后台,用于前端交互,实现下拉菜单里的部门信息数据是从后台获取,然后将选中的数据发回给后端进行处理。
实现方法
1. 静态生成的 dist 共享文件夹的方式
执行 npm run build 命令构建 vue 前端部分,然后将构建生成的 dist 文件夹定位到 my-tree-server 和 my-tree-selector 的父文件夹下作为前后端的共享文件夹。
此方法优点是,不需要运行 vue 项目,只需启动后端 flask 服务即可访问 vue 项目界面;缺点是,由于 dist 文件夹是通过 npm run build 生成的,是静态文件,当我们修改 vue 源文件的,修改的部分不会自动更新到后端服务器。
具体步骤如下:
- 在现有的 vue 项目文件夹的所在的文件目录下,新建一个后台文件夹 my-tree-server,然后在此文件夹中通过 virtualenv 命令创建虚拟目录,在启动虚拟目录之后安装 flask 等依赖;
- 在 vue 文件夹 config/index.js 修改下面的两行:
1
2
3
4
5
6
7
8 > // 修改前
> index: path.resolve(__dirname, '../dist/index.html'),
> assetsRoot: path.resolve(__dirname, '../dist'),
>
> //修改后
> index: path.resolve(__dirname, '../../dist/index.html'),
> assetsRoot: path.resolve(__dirname, '../../dist'),
>
- 在 my-tree-server 文件夹下新建 main.py 文件,内容如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 > from flask import Flask, render_template
>
> # 这里尤其要注意:两个目录都是../dist而不是./dist,../dist表示在根目录(my-tree-server)的父级目录下的dist目录,而./dist则表示my-tree-server下的dist目录。
> app = Flask(
> __name__, static_folder="../dist/static", template_folder="../dist")
>
>
> @app.route('/')
> def index():
> return render_template("index.html")
>
>
> if __name__ == '__main__':
> app.debug = False
> app.run(host='localhost', port=5000)
>
上面两步修改的目的,是为了将 vue build 生成的 dist 文件夹放在 my-tree-selector 和 my-tree-server 同一个根目录下,将 flask 的 template 和 static 都存放于 dist 目录下,从而实现 HTML 文件和静态文件的共享;
最后直接在 my-tree-server 文件目录的虚拟环境下运行
python main.py runserver
,即可在 localhost:5000 下直接访问之前 vue 创建的项目了。
2. 通过__webpack_hmr (webpack 热替换文件)实现动态生成
通过观察发现,Vue 项目在开发时,通过 webpack 热替换的编译代码中,含有 app.js(包含打包后的所有 HTML、js、CSS 文件) 和__webpack_hmr(webpack 热替换文件)两个文件,因此我们可以通过让 flask 引用这两个文件从而实现动态的更新和同步。
此方法的优点是可以自动同步更新,不需要每次更新 vue 内容之后都要重新 run build;缺点是需要同时运行前端 vue 服务器和后端 flask 服务器。
具体步骤如下:
- 在 flask 后端文件夹下创建 templates 和 static 文件夹,分别用于存放模板文件和静态文件;
- 将 vue 文件夹下的 index.html 模板复制到 templates 文件夹下,作为首页入口;
- 在 templates 文件夹下的 index.html 中添加一行引用如下,这里的 app.js 就是 vue 编译之后包含所有样式和 js 文件的页面文件:
1
2 > <script type="text/javascript" src="http://localhost:8080/app.js" />
>
- 在 flask 的 main.py 文件中增加一条重定向路由如下,这条路由的目的就是实现两边的同步:
1
2
3
4 > @app.route('/__webpack_hmr')
> def npm():
> return redirect('http://localhost:8080/__webpack_hmr')
>
遇到的问题
1. 在父组件中通过 axios 从 flask 后端获取的 json 数据是异步获取的,数据是在父组件和子组件都挂载完成之后才加载完,导致子组件的 props 不会自动同步异步加载的数据,从而导致组件渲染出错,数据为空。
这是由于 axios 是异步加载的,在数据加载完成之前组件就已经创建并且注册完成了,而 vue 的数据自动更新必须是初始化时就存在的属性,在这里初始化时,所有的 data 数据项都是空数组,所以后续直接给父组件 data 数组赋值不会触发 props 的自动同步更新。
具体解决的方法这里有一篇博客讲了好几种方案,最后我实现了其中两种比较简单直接的方式:
第一种方法
- 首先,将子组件设计为 v-if 条件加载的方式,然后通过设置一个条件变量
dataReady
,来控制子组件的显示与否;- 然后,在父组件钩子函数 created 中通过 axios 异步向 flask 后端接口请求 json 数据,在 axios 回调函数中将 response 中的数据赋值给 appartmentList;
- 接着,使用 watch 监听父组件中 appartmentList 数组的变化,一旦 axios 异步加载完毕,回调函数中修改了 appartmentList 数组,在 watch 函数中立马将
dataReady
条件改为 true;- 当 v-if 条件为 true 时,子组件才会开始渲染,此时父组件数据也已经准备好了,就不会出现子组件数据为空的问题了。
此方法主要是利用了 v-if 指令的动态渲染特性,从而实现在数据准备好之后再渲染子组件。
此方法优点是使用简单,只需要通过一个 dataReady 变量控制,在 MyTreeSelector 组件中什么也不用修改;缺点是当需要加载的数据量比较大时,组件等待渲染的时间可能会比较长,造成页面空白。第二种方法
- 首先,同上一种方法,在父组件钩子函数 created 中通过 axios 异步向 flask 后端接口请求 json 数据,在 axios 回调函数中将 response 中的数据赋值给 appartmentList,赋值的同时会同步到子组件的 props 属性中;
- 在子组件中添加 watch 属性,监听 props 中 appartmentList 的变化情况;
- 一旦监听到 props 的 appartmentList 发生变化,立即触发 updateData 方法,手动更新数据。
此方法主要是利用了子组件的 watch 特性以及子组件 props 与父组件保持同步的特性,从而可以实现手动更新。
此方法的优点是所有逻辑全部在子组件内部完成,父组件无需多余的操作就可以直接兼容异步和同步的数据,而且没有数据的时候组件也能正常渲染,只是数据选项为空而已,不会出现空白;缺点是需要手动触发更新操作,与 vue 父子组件双向数据流的理念有点出入。
2. Element UI Notice 部分的子组件引入之后,在页面刷新时会自动弹出
单独引用 Notice 里面的子组件是不需要 Vue.use 的,直接在需要的地方直接引用就可以,使用方法如下:
Message(options)
,或者还可以通过手动将对应的组件绑定到 Vue.prototype 上面来使用,例如:Vue.prototype.$message = Message;this.$message(options)
。
3. python 获取数据时获取到的值为 None
- 方法一:前端 axios 直接传过来的是原始的 json 格式的数组,在后端直接用
request.json
就可以获取到正确的数据值;- 方法二:前端先调用
JSON.stringify()
将原始数组进行 json 序列化,然后再借助axios.interceptors
对 post 请求进行拦截,在发出 post 请求之前将 post 的 data 调用qs.stringify()
转换成查询字符串的形式,最后在后端通过request.values.get()
获取到对应的数据项,并通过json.loads()
将数据字符串解码成 python 的数据格式。
4. python 中的 json 几种方法对比:json.load、json.loads、json.dump、json.dumps
json.load
: 将某个文件对象中的数据按照 json 格式化反序列化为 python 的数据对象格式,例如:[]数组变成 python list 对象;{}对象变成 pythondict 对象。json.loads
: 将某个 json 字符串反序列化为 python 的数据对象格式。json.dump
: 将某个 python 数据格式的对象序列化为 json 字符串的形式,并且可以写入某个文件对象中,存储到本地。json.dumps
: 将某个 python 数据格式的对象序列化为 json 字符串的形式,返回的是序列化后的 json 字符串。
第四周
python2.x 编码问题
在 python 后端采用‘+’直接拼接字符串和 request 获取的变量的时候经常会报错,主要有以下两种:
UnicodeDecodeError: 'ascii' codec can't decode byte 0xe6 in position 44: ordinal not in range(128)
TypeError: coercing to Unicode: need string or buffer, NoneType found
TypeError: cannot concatenate 'str' and 'int' objects
第一种问题主要是由于字符串中包含了中文字符导致的,由于 python2 默认的字符串编码是 ASCII 编码,所以对于中文字符是没办法自动转化为 Unicode 编码的,所以会报错,针对这种错误,解决方法就是在声明字符串的时候采用
u'字符串'
的形式直接生命为 Unicode 格式的字符串;
对于第二种问题,主要是由于 python 后端 flask 通过 request 获取表单元素值的时候,有些表单元素不是必填项,所以值可能为 None,导致字符串拼接出错。针对这种问题,解决方法就是在字符串拼接前先手动判断一下变量是否为 None 值,然后再相加;
针对第三种问题,主要是由于 python 的‘+’操作默认只能再相同类型的两个变量之间进行,数字+数字,str+str,针对这种问题的解决方法是,先将两个变量转化为同一类型的变量再相加。