因公司业务需要,去往远方开发项目,大半年来除了加班还是加班,开发了三个后台系统,使用的vue-element-admin框架,该文章如标题,为个人记录。
vue-element-admin
该框架作者有写了两篇webpack教学文章,其中详细分析了为何该框架为何这么配置,两篇文章建议仔细阅读。得益于webpack 4+vue/cli3+作者做的配置,基本已经达到开箱即用了,不过多介绍
手摸手,带你用合理的姿势使用webpack4(上)
手摸手,带你用合理的姿势使用webpack4(下)
vue-cli不支持 eject 来弹出默认配置
当初刚开始看的时候想要看一下目前vue-cli究竟做了哪些默认配置(官网写的一般不会完全覆盖,所以想自己查看默认配置),可惜并没有类似 react 的 eject来弹出
关于为何没有可查看以下链接,比较赞同官方,为了一些特定场景而去加大他们的工作量,并且并不能带来实质收益,对到框架开发来讲不合理
Eject from Vue CLI / Export Webpack Config
查看vue/cli默认配置
如果不清楚vue/cli做的默认配置,那么遇到问题的时候其实只靠网上搜索很难找到自己遇到的问题,因此先了解vue/cli的默认配置,方便出现问题的时候查看是否有某些配置没更改,进入项目,在项目根目录下
- 运行命令,在终端输出:
开发环境:npx vue-cli-service inspect --mode development
生产环境:npx vue-cli-service inspect --mode production - 运行命令,将输出导入到 js 文件:
开发环境:npx vue-cli-service inspect --mode development >> webpack.config.development.js
生产环境:npx vue-cli-service inspect --mode production >> webpack.config.production.js
参考地址:
vue-cli-service-inspect
修改插件选项
动态路由无法分包
该问题直到项目后期才发现,因此已经没法追溯究竟什么时候开始构建的时候分包失败
由于项目中的菜单需要根据权限进行管理,角色权限没有的菜单栏不会显示到侧边栏上,所以由后台返回对应的页面级组件地址(字符串,例如'./view/order'
),前端使用require()来获取正式的component(其结果类似于route中component: './view/order'
)
参考地址:
Webpack-Vue 分片优化——为什么使用懒加载() => import() 里面的组件没有分片打包
后改为映射表,通过路径来映射前端component,再使用import来导入对应的component(并非真正原因,因为vue/cli3默认就是可以对动态路由进行分包的,改完之后依旧无效。自行配置spliteChunks后虽然成功分包了,但是动态路由也失效了,没法按需加载,直接一进首页全部请求下来了),该记录仅为让自己以后如果出现问题还能往这方面考虑
真实预发布环境增加了一个pre环境(也就是多了一个.env.preprod文件),虽然里面已经指定了NODE_ENV=production,然而发现一个奇怪的bug,就是不管怎样都无法分包(代码压缩等都正常),并且该bug表现为:
- 如果先执行build:prod,则正常分包,此时再执行build:pre,结果也会分包
- 如果先执行build:pre,则无法分包,此时再执行build:prod,结果也无法分包
- 如果更改一下chunkName,再执行build:prod,又会正常分包
- 删除node_modules中的缓存,执行build:prod,也会恢复正常分包
这里涉及知识盲区,不继续深究,以后技术深度有所突破能知道原因再来更新此文,后来与后台协定,预发布与正式均使用build:prod,不再区分环境(因为对到前端来讲其实预发布与正式环境代码都是同一份)
打包后的app.css巨大
构建后发现app.css包居然达到15M,饿了么样式被重复打包
优化:去除入口文件引入的饿了么UI默认样式(官网有说自定义样式不需要引入默认样式,仅作提示),将vue-element-admin中主题配置文件拆分(因为项目中可能会有很多需要引用主题色变量的,直接从该配置文件获取主题相关配置样式,会导致饿了么样式被重复打包)
参考链接:
自定义主题样式文件多次打包 ,解决方法为Tofandel这名用户所说的,不要将变量与自定义导入主题放在一块
icon-font打包后乱码,浏览器能正常显示
该问题表现为打包结果乱码(为一个空芯方块),但代码在浏览器中可正常显示iconf-font。打开控制台再刷新,则第一次icon-font必定展示乱码(也就是为什么Unicode明文在F12的时候会有一次无法识别)
百度出来的肯定是将dart-sass换成node-sass,但根据自行查看issue,dart-sass确实会导致编译为Unicode 明文,但貌似并非底层真正原因。由于时间也是换成node-sass来解决,先做记录,后续再看issue
用最新的框架,打包出来element的字体图标乱码了? #3526
页面刷新有时候elementui 的字体图标会乱码 #19247
现代浏览器type="password"自动填充密码
查看了很多文章,全体方案阵亡,包括:
- 设置autocomplete
- 通过动态设置readonly
- 动态更改type为password
- 增加两个autocomplete="off"的隐藏密码输入框
不管哪一个方案,在切换成password的时候,自动填充又会出现,通过stackoverflow了解到之前这些方案都行不通了,原本打算自行实行模态密码框来模拟密码输入框(本质还是text)
但通过stackoverflow了解到可以将type设置为text,通过css设置text-security: disc;
来模拟密码输入框,但这个时候下面也有人说了复制粘贴会复制到全都是小圆点,但密码本来就不该让用户复制粘贴,所以再将密码框的粘贴事件禁用即可onpaste="return false"
如何阻止浏览器自动填充账号密码
下面这篇文章我没尝试,稍微看了一下也是动态设置type和readonly这些方案,但是多管齐下,不确定是否有用,有兴趣的可以自行尝试
完美解决 element-ui input=password 在浏览器会自动填充密码的问题
封装后的v-password组件
前端文件下载
前端文件下载最简单的方式是使用download属性
文件名
但是这种下载方法不能跨域,非同域download会无效。而使用新开页面下载的方式会有一个弹窗闪一下影响体验。由于项目中有需要下载第三方文件的需求,还有私有桶提供的blob文件流,所以更改为使用文件id请求模拟点击下载,代码如下
// 工具函数
const Tool = {
_getBlob (ret, fileName, file) {
const type = ret.type
const blob = new Blob([ret], { type }) // type必须指定,即使时流文件,否则火狐下载无后缀
const ie = navigator.userAgent.match(/MSIE\s([\d.]+)/)
const ie11 = navigator.userAgent.match(/Trident\/7.0/) && navigator.userAgent.match(/rv:11/)
const ieEDGE = navigator.userAgent.match(/Edge/g)
const ieVer = (ie ? ie[1] : (ie11 ? 11 : (ieEDGE ? 12 : -1)))
if (ie && ieVer < 10) {
Message.error('您的浏览器版本过低,请切换到IE EDGE模式或更换浏览器')
}
if (ieVer > -1) {
window.navigator.msSaveBlob(blob, fileName)
} else {
const url = window.URL.createObjectURL(blob)
file.href = url
}
},
/**
* @description 文件下载,非流文件(如视频音频等)
* @param {String, Array} id 文件id,如果是批量下载必须传入数组id集合
* @param {String} fileName 保存时候的文件名
*/
async downLoad (id, fileName = '') {
const file = document.createElement('a')
const body = document.querySelector('body')
let ret = {}
if (!Array.isArray(id) || !id.length) {
Message('下载参数异常')
return
}
// 业务接口
ret = await fileModel.zipFile(fileName, id)
if (!ret.size || ret.size < 1024) {
Message.error('文件异常,该文件无法下载')
} else {
fileName = fileName || ret.fileName // 实现前端自定义文件名或使用后台返回的文件名,需要在request.js补充
this._getBlob(ret, fileName, file)
}
// IE和火狐必须制定下载的格式,否则下载后丢失文件后缀,目前来看会有类型,因为之前没有content-type?
file.download = fileName
file.style.display = 'none'
body.appendChild(file)
file.click()
body.removeChild(file)
window.URL.revokeObjectURL(file)
}
}
request.js
service.interceptors.response.use(
response => {
const contentType = response.headers['content-type'].toLowerCase()
// 返回请求体是流文件
if (contentType.includes('octet-stream') || contentType.includes('vnd.ms-excel') || contentType.includes('zip')) {
if (response.status === 200) {
// 将后台返回的晴天球头文件名填充到响应体中
response.data.fileName = window.decodeURI(response.headers['content-disposition'].split('=')[1])
return Promise.resolve(response.data)
} else {
Message({
message: '文件下载失败,请稍后尝试',
type: 'error',
duration: 2 * 1000
})
}
}
}
)
vue-echarts
项目中使用了百度可视化插件ECharts,本来想直接使用vue-echarts,但用vue-charts一直偶现实例化时必要数据无数据(options中的数据),导致一直报错,github有同样的issuse但作者并无理会,所以直接放弃,自行封装了一个简易版的v-charts,options为echarts实例所需的对象
resize.js
import { debounce } from '@/utils'
export default {
data () {
return {
$_sidebarElm: null,
$_resizeHandler: null
}
},
mounted () {
this.initListener()
},
// 假如页面走缓存,则离开页面销毁
activated () {
if (!this.$_resizeHandler) {
this.initListener()
}
this.resize()
},
deactivated () {
this.destroyListener()
},
beforeDestroy () {
this.destroyListener()
},
methods: {
$_sidebarResizeHandler (e) {
if (e.propertyName === 'width') {
this.$_resizeHandler()
}
},
initListener () {
this.$_resizeHandler = debounce(() => {
this.resize()
}, 100)
window.addEventListener('resize', this.$_resizeHandler)
// 侧边导航条因为不会触发浏览器resize,所以需要进行事件监听
this.$_sidebarElm = document.getElementsByClassName('sidebar-container')[0]
this.$_sidebarElm && this.$_sidebarElm.addEventListener('transitionend', this.$_sidebarResizeHandler)
},
destroyListener () {
window.removeEventListener('resize', this.$_resizeHandler)
this.$_resizeHandler = null
this.$_sidebarElm && this.$_sidebarElm.removeEventListener('transitionend', this.$_sidebarResizeHandler)
},
resize () {
const { echart } = this
echart && echart.resize()
}
}
}
富文本编辑器Tinymce
网上评测文章很多,不再赘述。主要强调的是版本,网上有一篇比较通用的文章,但版本是4x的,下载的时候已经是5x,并且tinymce-vue也已经到2x,因此不能跟着4x的方式来做,本人使用的版本如下:
"tinymce": "5.0.3", "@tinymce/tinymce-vue": "2.0.0"
vue-element-admin原作者里面也有封装了一个tinymce插件,但使用的CDN的方式通过script插入。由于为真实项目,使用这种方式不太合适,因此自行封装了一版tinymceEditor组件,图片自动上传(可截图)
完整步骤:
- 下载合适的版本(大部分报错都是因为tinymce与tinymce-vue不匹配,如果有问题可固定为上述版本)
- public文件夹中新增tinymce文件夹,将node_modules中
tinymce
里的skins
复制进去,可删除多余的文件,仅剩所需的css文件即可
-| public
-| tinymce
-| skins
-| ui
-| oxide
-| content.min.css
-| skin.min.css
- 下载中文包(看个人需要),下载链接tinymce语言包
- 导入tinymce、tinymce-vue、zh_CN.js中文包与所需的插件,语言包我是放在资源目录中的,具体根据项目规范自行放置导入即可
- 如果项目中tinymce是在弹层当中,下拉菜单会因为层级过低导致不可见,需要自行调整z-index,没有什么好办法,本人解决方案是通过!important来调整层级的
.tox-tinymce-aux {
z-index: 2021 !important;
}
完整代码:
使用方法
跨组件,将按钮插入到头部navbar中
如上图所示,一开始原型所有按钮都在下方红框当中,根据逻辑展示对应按钮(因此开发的时候都写在业务组件之中)。
后来设计出视觉的时候将所有区块按钮转移到头部navbar当中(出现该问题的原因很多,从流程到相关人员专业度等等,这里就不吐槽了)。此时有一个致命的问题:超级多页面都有该功能,且都有不同的逻辑在其中,如果从框架上进行更改,工作量极大。
这里跨越的组件层级已经无法通过常规的传输方式来解决(不仅是跨组件,而且是跨了N多个组件文件)。要解决这个问题的前提是必须保证现有逻辑的运行,原本的v-if或v-show也能正常判断
因此开发了一个指令与一个组件,用于该场景,解决思路是将通过指令,将原有的节点插入到navbar中去。由于navbar在系统中仅有一个,所以写了一个带ID的空白节点作为落脚点,具有该指令的节点初始化则直接不可见。
/**
* @author yose
* @description 该指令仅适用于页面没走缓存(非keep-alive),否则需要使用组件sysbtn
*/
const install = function (Vue) {
Vue.directive('sysbtn', {
bind (el) {
el.style.display = 'none'
appendToSysbtn(el, Vue)
},
update (el) {
appendToSysbtn(el, Vue)
},
unbind (el) {
const sysbtnDom = document.getElementById('sys_btn')
if (!sysbtnDom) { return } // 路由路径为退出/404等非layout界面,无法获取到该dom节点
const childrenNodes = Array.from(sysbtnDom.childNodes)
childrenNodes.forEach(item => el.appendChild(item))
}
})
}
function appendToSysbtn (elm, vue) {
vue.prototype.$nextTick().then(() => {
const childrenNodes = Array.from(elm.children)
const sysBtnNode = document.getElementById('sys_btn') // navbar中被插入的节点,上图最后插入的位置
childrenNodes.forEach(item => sysBtnNode.appendChild(item))
})
}
export default install
如果页面走缓存(也就是keep-alive),该指令在离开页面后也无法释放按钮(因为不会触发unbind),所以需要一个功能一致的组件来监听页面的进入activated
与离开deactivated
使用方式示例(主要是想说明原来的代码除了增加v-sysbtn指令,其余都无需更改就能完成新的视觉需求):
订单加/扣款
提交完成
组件上
查询
新增
主要为提供一个思路,不要仅限于组件间传值,遇到部分场景不剑走偏锋,会导致需求难度极度复杂,并且耗费大量的无意义时间