
前言 最近在做交通优化分析工具的产品时,有一个需求是用户上传一份包含路段信息的csv文件,后端需要解析csv的文件内容并将信息插入数据库中。这是一个常规的操作,也不复杂,但是在实现的过程中却踩到了一个utf-8 BOM的坑,随手记录一下。 实现方式 完整的实现方式如下: 在spring中通过MultipartFile file这个对象来接受前端传过来的文件 获取file对象的InputStream输入流 将上一步的输入流和定义好的DTO对象传给opencsv的CsvToBeanBuilder方法, CsvToBeanBuilder方法会自动解析输入流中的内容并生成对应的DTO List 最后根据业务需求,生成相应的DO对象存入数据库 前面有坑 csv文件样例: path_id,path_name 1,文一路 2,文二路 DTO定义: @Data public class CsvDTO { @CsvBindByName(column = " path_id", required = true) private String pathId; @CsvBindByName(column = "path_name") private String pathName; } 其中, @CsvBindByName注解中的require = true表明这是一个必须存在的字段 当我上传了这个样例文件后,代码报错了: java.lang.RuntimeException: Error capturing CSV header! ... Caused by: com.opencsv.exceptions.CsvRequiredFieldEmptyException: Header is missing required fields [PATH_ID]. The list of headers encountered is [ path_id,rid,path_name]. 我上传的csv文件里明明有path_id这个字段,为什么报的错是字段找不到。这个错误十分的迷惑,以至于我拼命的在找csv文件的首行有什么问题。 找了半天之后没有发现什么问题,于是我就换了个思路,毕竟csv文件只是用逗号分隔的纯文本,我可以自己手写一个csv文件,于是我在编辑器里敲了一个新的csv出来,重新上传。结果代码正常的跑完了,没有报错。 那么问题会在哪里 我分别用excel打开这2个csv文件,结果发现 一开始的样例文件 手打的csv文件 看到这样的结果想起来在Windows上经常遇到的文件乱码问题,原因是文件头不存在BOM,那时是用notepad++来转换格式: 那么我现在遇到的这个报错会不会是因为文件头存在BOM呢? 为了验证这个想法,我把服务器接收到的文件内容按字符打出来: 果真,文件的第一个字符不是p,而是\uFEFF,正是utf-8 BOM。 于是,这个问题的答案已经有了,我指定了path_id列必须存在,而opencsv按“逗号分隔”的标准定义,认为文件里有一列叫\uFEFFpath_id,却找不到path_id,于是就报错了。而报错的迷惑性在于\uFEFF是不可见字符,其实报错的时候有打出来,只是看不见而已: The list of headers encountered is [ path_id,rid,path_name]. ^ 看似空格,其实是\uFEFF 解决问题 问题的原因已经找到,接下来就是如何解决这个问题,一般到这个时候的解决办法也不会太复杂,一种方法是每次读文件的时候做一个判断是否存在BOM,如果存在就去掉,然后把结果再交给opencsv处理。另一种方法是apache提供了一个BOMInputStream类,能自动识别是否存在BOM以及去除BOM: // ... BOMInputStream bomInputStream = new BOMInputStream(file.getInputStream()); new CsvToBeanBuilder(new InputStreamReader(bomInputStream)) // ... 改完之后,服务器就能欢快的接收各种带BOM和不带BOM的文件了。 references https://en.wikipedia.org/wiki/Byte_order_mark https://commons.apache.org/proper/commons-io/javadocs/api-2.5/org/apache/commons/io/input/BOMInputStream.html
普通middleware const express = require('express') const app = express() const port = 3000 app.use('/normal', (req, res) => res.send('Hello World!')) app.listen(port, () => console.log(`Example app listening on port ${port}!`)) 文艺middleware (async) const express = require('express') const app = express() const port = 3000 app.use('/art', async (req, res) => { await new Promise((resolve, reject) => { setTimeout(resolve, 1000) }) res.send('Bonjour le monde!') }) app.listen(port, () => console.log(`Example app listening on port ${port}!`)) 万一middleware里报错了怎么办 增加一个4参数的中间件作为error handler,当然express也有一个默认的handler const express = require('express') const app = express() const port = 3000 app.use('/normal', (req, res) => res.send('Hello World!')) app.use('/normal-error', (req, res, next) => { throw new Error('some error') }) app.use((err, req, res, next) => res.send('error detected: ' + err.message)) app.listen(port, () => console.log(`Example app listening on port ${port}!`)) async middleware里的报错也这么处理? 答案是No const express = require('express') const app = express() const port = 3000 app.use('/art', async (req, res) => { await new Promise((resolve, reject) => { setTimeout(resolve, 1000) }) res.send('Bonjour le monde!') }) app.use('/2b-error-1', async (req, res) => { await new Promise((resolve, reject) => { setTimeout(() => reject('another error 1'), 1000) }) }) app.use('/2b-error-2', async (req, res) => { await Promise.resolve().then(() => { throw new Error('another error 2') }) }) app.use((err, req, res, next) => res.send('error detected: ' + err.message)) app.listen(port, () => console.log(`Example app listening on port ${port}!`)) 如果只这样写的话,浏览器里不会有任何返回,而你会在终端里得到下面的报错: 正确处理async middleware里的报错 const express = require('express') const app = express() const port = 3000 app.use('/art', async (req, res) => { await new Promise((resolve, reject) => { setTimeout(resolve, 1000) }) res.send('Bonjour le monde!') }) app.use('/art-error-1', async (req, res, next) => { try { await new Promise((resolve, reject) => { setTimeout(() => reject(new Error('another error 1')), 1000) }) } catch (e) { next(e) } }) app.use('/art-error-2', async (req, res, next) => { await Promise.resolve().then(() => { throw new Error('another error 2') }).catch(next) }) app.use((err, req, res, next) => res.send('error detected: ' + err.message)) app.listen(port, () => console.log(`Example app listening on port ${port}!`)) 处理的办法就是catch住异步操作中可能会抛出的错误,这里其实没有新的内容,在express的文档里已经说了必须捕获异步操作中的错误: You must catch errors that occur in asynchronous code invoked by route handlers or middleware and pass them to Express for processing. 每个异步操作都如此try/catch,太麻烦了 解决办法是写一个高阶函数asyncMiddleware,在这个函数里面给传进来的middleware自动加上catch方法。 const express = require('express') const app = express() const port = 3000 app.use('/art', async (req, res) => { await new Promise((resolve, reject) => { setTimeout(resolve, 1000) }) res.send('Bonjour le monde!') }) // 重点 function asyncMiddleware(fn) { return (req, res, next) => fn(req, res, next).catch(next) } app.use('/art-error-1', asyncMiddleware(async (req, res, next) => { await new Promise((resolve, reject) => { setTimeout(() => reject(new Error('another error 1')), 1000) }) })) app.use('/art-error-2', asyncMiddleware(async (req, res, next) => { await Promise.resolve().then(() => { throw new Error('another error 2') }) })) app.use((err, req, res, next) => res.send('error detected: ' + err.message)) app.listen(port, () => console.log(`Example app listening on port ${port}!`)) 如果不确定参数是否一定为async function,可以使用Promise.resolve(fn(req, res, next))
honeypack honeypack是一个基于webpack,结合了不同项目开发习惯,编写而成的开源前端打包工具。 功能 支持独立启动一个dev server 支持作为express的中间件 支持纯打包模式 特性 通过简单地问答就能生成一份完整实用的webpack配置文件,并且自动安装依赖,接着你可以根据自己项目的特点随意修改,想加loader随意加,想改plugin的参数随意改,不想要的配置随意删,没有做不到的定制内容。 [计划中]自动升级webpack配置文件,支持最新特性。比如自动把uglifyjs换成terser,让你把全部精力都放在app开发上。 了解更多 HMR HMR-热更新指的是当前端任何被引用到的文件发生变化时,服务器能自动推送新的文件到浏览器,并且能把修改的地方立即体现出来,而不用刷新页面。 了解更多 如何在honeypack中支持HMR 这里只介绍在中间件形式下开启HMR的方法,如果是独立启动一个server,参考官方文档 设置参数hot为true // honeycomb webpack: { enable: true, module: 'honeypack', router: '/assets', config: { hot: true // <------- 这个 } } // express app.use(honeypack({ config: 'webpack.config.js', root: './assets', hot: true // <------- 这个 })); 修改webpack.config.js文件 entry: { - app: './index.jsx' + app: [ + `honeypack/client?path=${publicPath}/__webpack_hmr`, + './index.jsx' + ] } plugins: [ + new webpack.HotModuleReplacementPlugin(), ] * publicPath为output中的publicPath 这个时候刷新一下页面,就会发现多了一个`http://${host}/${publicPath}/__webpack_hmr`的请求。 修改前端项目代码 集成更新 react 给顶层组件加上hot()方法 import React from 'react'; import ReactDOM from 'react-dom'; import {hot} from 'react-hot-loader/root'; const App = () => <div>Hello Word!</div>; const Wrap = hot(App); ReactDOM.render(<Wrap />, $DOM); 刷新一下浏览器页面,看见`Hello Word!`后,在文件里把`Word`改成`World`,回到浏览器,就会看见已经是`Hello World!`了 + [推荐的可选步骤] 在`webpack.config.js`中,给`babel-loader`增加一个plugin // babel-loader plugins: [ + 'react-hot-loader/babel' ] 2. [其他框架](https://webpack.js.org/guides/hot-module-replacement/#other-code-and-frameworks) 2. 手动更新 手动更新的思路是服务器会主动推送被修改过的文件,然后前端根据不同的文件手动进行不同的操作。 [了解更多](https://webpack.js.org/guides/hot-module-replacement/#enabling-hmr)
细心的朋友们可能会发现DataV的编辑器右侧多出了一个“交互”tab,有没有好奇它是做什么的? 接下来就由小编带领大家一探究竟 配置 入门篇 首先来回顾一下什么是回调id,在DataV中回调id是指某个组件在响应用户操作或者自动触发更新时向别的组件传递的参数,这个参数可以在别的组件中用于数据查询时的动态变量。例如用户在地图组件上点击了某个省份后,分组柱图会把选中的省份作为参数去查询各个市的统计数据,或者时间轴组件的时间发生变化时,轮播列表组件会把时间作为参数去获取新的数据。 这次我们在回调id的基础上,提供一个独立编辑区块,方便大家清晰、快速的用好回调id这一特性。 以数字翻牌器为例,该组件在数字发生变化时它可以向别的组件提供一个参数value(下图中的“字段”,对应数据源中的字段),当我们点击右上角的「启用」按钮时,系统将自动设置一个同名变量value(下图中的“绑定到变量”)指向这个参数。 当然,大家也可以修改“绑定到变量”中的变量名称,如下图所示,我们将value修改为income,这时在别的组件使用该变量时就可以使用income来取得这个参数。利用这一特性,我们就可以给不同的组件设置不一样的变量名称,达到区分使用不同参数的目的。 高级篇 1.设置自定义字段 此外,在组件的数据源配置中还支持设置除组件必需字段外的数据,例如我们给数字翻牌器额外设置一个id字段,值为123。 这时回到编辑「交互」的地方,点击“新建一个字段”,在“字段”的地方填入id,在“绑定到变量”的地方填入你想设置的变量名称。注意,只有在同时填写了“字段”和“绑定到变量”后,这个变量才会生效。 2.设置回调id的默认值 我们可以通过在url中设置请求参数的形式来设置回调id的默认值,如: http://datav.aliyun.com/screen/000000?myid=123 注释:000000表示屏幕id 通过这个url访问时,在页面打开的时候,回调idmyid的值已经设置为123了。 多个回调id之间使用&符号连接,如 http://datav.aliyun.com/screen/000000?myid=123&income=1000 注释:000000表示屏幕id 这里同时设置了回调idmyid和income的默认值 使用 回调id的使用跟以前保持一致,在数据源中使用:变量名(如:abc)即可。 例如:SQL: select :name as value select A from table where year = :name API: http://api.test?income=:income&id=:myid One more thing 我们增加了回调参数自动补全的功能,也就是说在配置数据源时,只要键入:,编辑器将提示当前屏幕下所有已经配置过的变量名称,可以使用上下键选择,回车键确定。当屏幕中有大量交互组件的时候,这个功能是不是很贴心呢,不用再去一个个点组件查看之前设置的变量是什么了。 使用案例 大数据美食——寻找地图上的美味 DataV带你回顾春节前后全国空气质量变化
2019年09月