个人项目记录

因公司业务需要,去往远方开发项目,大半年来除了加班还是加班,开发了三个后台系统,使用的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表现为:

  1. 如果先执行build:prod,则正常分包,此时再执行build:pre,结果也会分包
  2. 如果先执行build:pre,则无法分包,此时再执行build:prod,结果也无法分包
  3. 如果更改一下chunkName,再执行build:prod,又会正常分包
  4. 删除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"自动填充密码

查看了很多文章,全体方案阵亡,包括:

  1. 设置autocomplete
  2. 通过动态设置readonly
  3. 动态更改type为password
  4. 增加两个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组件,图片自动上传(可截图)
完整步骤:

  1. 下载合适的版本(大部分报错都是因为tinymce与tinymce-vue不匹配,如果有问题可固定为上述版本)
  2. public文件夹中新增tinymce文件夹,将node_modules中tinymce里的skins复制进去,可删除多余的文件,仅剩所需的css文件即可
-| public
  -| tinymce
    -| skins
      -| ui
        -| oxide
          -| content.min.css
          -| skin.min.css
  1. 下载中文包(看个人需要),下载链接tinymce语言包
  2. 导入tinymce、tinymce-vue、zh_CN.js中文包与所需的插件,语言包我是放在资源目录中的,具体根据项目规范自行放置导入即可
  3. 如果项目中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指令,其余都无需更改就能完成新的视觉需求):

订单加/扣款 提交完成

组件上


主要为提供一个思路,不要仅限于组件间传值,遇到部分场景不剑走偏锋,会导致需求难度极度复杂,并且耗费大量的无意义时间

你可能感兴趣的:(个人项目记录)