热点后台发布系统
热点后台发布系统项目总结
业务逻辑思路以及遇到的一些问题
- 登录通过后端返回的 token 值来进行跳转,并对其进行路由拦截处理,并封装本地存储函数,让 Token 进行数据持久化
- 内容管理模块则对数据进行渲染,其中运用 el-table 进行表格渲染,其中想要自定义列中的内容要用 template 模板进行编译,其中想要拿到遍历的对象数据,要运用 slot-scope="scope"拿到数据进行下一步操作,其中图片运用了 el-image 进行懒加载处理。
- 内容管理模块的筛选业务,就是将其每个数据渲染项的父元素绑定其要提交的参数,请求带参,因为请求本身不带参数后台有设置默认参数,最终设置了所有参数。
- 发布文章模块,使用了 element-tiptap 第三方库,这个库是对 tiptap 进行 element 方法的封装,在路由导航过来传递的 id 可能有过长的原因失真,然后通过 json-bigint 第三方库进行解决,在 request 请求模块中 transformResponse 中去 return jsonBig.parse(data),在使用数据的使用进行 toString 方法。
- 在图片上传中有个问题,就是上传的图片是渲染在标签中的,因为图片是进行编码的,这个编码会很长很长,所以要进行处理,
new Image({
async uploadRequest(file) {
//将文件对象转为formdata
let fd = new FormData();
fd.append("image", file);
//请求线上的url地址,最终返回
const { data: res } = await getImgUrl(fd);
return res.data.url;
},
}),
-
还有 element 组件中有些组件@click 事件不起效,可以通过.native 修饰符让其生效
-
接口文档说明可能不完整,昨天遇到一个问题,响应报文中有个 value does not match pattern,值不匹配。以后出错看响应报文到底报什么错,仔细理解其含义
-
文章评论模块中有一个让el-switch开关让其在loading状态中并禁用点击,但是接口中没有loading这种属性。
-
解决方案:所以将其拿到的数组添加一个属性,默认为false,当loading状态开启时,禁用的属性同时也是为true,:disabled绑定的就是这个添加的loading属性
-
//添加一个statusLoading开关加载状态,默认设置false,为了防止点击很多次发送多次请求 results.forEach((result) => { result.statusLoading = false; });
-
-
-
素材管理模块其中素材列表进行了封装,可以进行组件定制。其中添加素材按钮使用了element-upload组件。
-
<!-- 文件上传 --> <!-- action设置请求地址,name设置上传的字段名,设置上传的请求头部 --> <el-upload class="upload-demo" action="http://api-toutiao-web.itheima.net/mp/v1_0/user/images" :headers="upLoadHeader" name="image" drag :on-success="onUploadSuccess" multiple :show-file-list="false" > <i class="el-icon-upload"></i> <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div> <div class="el-upload__tip" slot="tip"> 只能上传jpg/png文件,且不超过500kb </div> </el-upload> -
上传必须携带token,也可以自己定义事件上传,只不过稍微有点麻烦。
-
//上传的请求头 upLoadHeader: { Authorization: `Bearer ${user.token}`, },
-
-
-
发布文章模块使用了一个element-tiptap富文本编辑器,并将上传封面单独封装成一个组件,由于发布文章和修改文章都是同一个页面,所以需要根据$route参数来判断发送请求。
-
this.loadChannel(); if (this.$route.query.id) { this.loadAssignArticle(); } -
修改和更改文章内容也是如此
-
onPublish(draft) { this.$refs["publishForm"].validate(async (valid) => { if (!valid) { return; } const articleId = this.$route.query.id; if (articleId) { //修改文章的操作 await updateAssignArticle(articleId, this.article, draft); this.$message({ message: "修改成功", type: "success", }); this.$router.push("/article"); } else { await publishArticle(this.article, draft); this.$message({ message: "发布成功", type: "success", }); this.article.title = ""; this.article.content = ""; this.$router.push("/article"); } }); },
-
-
关于富文本编辑器element-tiptap进行一个总结
-
先安装
-
npm i element-tiptap
-
-
初始配置
-
<template> <el-tiptap v-model="content" :extensions="extensions"></el-tiptap> </template> <script> import { ElementTiptap, Doc, Text, Paragraph, Heading, Bold, Underline, Italic, Image, Strike, ListItem, BulletList, OrderedList, TodoItem, TodoList, HorizontalRule, Fullscreen, Preview, CodeBlock } from 'element-tiptap' import 'element-tiptap/lib/index.css' export default { components: { 'el-tiptap': ElementTiptap }, data () { return { content: 'hello world', extensions: [ new Doc(), new Text(), new Paragraph(), new Heading({ level: 3 }), new Bold({ bubble: true }), // 在气泡菜单中渲染菜单按钮 new Image(), new Underline(), // 下划线 new Italic(), // 斜体 new Strike(), // 删除线 new HorizontalRule(), // 华丽的分割线 new ListItem(), new BulletList(), // 无序列表 new OrderedList(), // 有序列表 new TodoItem(), new TodoList(), new Fullscreen(), new Preview(), new CodeBlock() ] } } } </script>
-
-
-
在上传封面组件中又一次使用了el-tabs组件,并对其imag-list素材库组件进行了个性化定制。其中文件上传使用了file类型的input框,在选择框div中设置input框的点击。
-
el-tabs 组件 v-model 双向绑定 数据驱动视图:当绑定数据发生改变,激活的标签页受影响 视图影响数据:当标签改变的时候,标签的 name 会同步到数据中 label 标签的标题 name 相当于标签的 id
-
-
在确定提交文件的时候,做判断,并且上传图片的化拿到的是文件对象,需要去请求接口返回url地址。
-
//确定提交文件 async onUploadCover() { if (this.activeName === "upload") { const file = this.$refs.uploadCover.files[0]; //上传图片 if (!file) { this.$message("请选择图片"); return; } const fd = new FormData(); fd.append("image", file); //上传用户素材返回url const { data: res } = await getImgUrl(fd); this.publishImage = res.data.url; //关闭对话框 this.uploadDialogVisible = false; this.$message({ message: "上传图片成功", type: "success", }); //向父组件传url // this.$emit("update-url", this.publishImage); this.$emit("input", this.publishImage); //隐藏加号元素 this.$refs["addImage"].style = "display:none"; } else if (this.activeName === "image") { //获取组件元素,通过refs方式 const imageList = this.$refs["imageList"]; const selected = imageList.selected; if (selected === null) { this.$message("请选择图片"); return; } //关闭对话框 this.uploadDialogVisible = false; //发送父组件图片地址 this.$emit("input", imageList.imageList[selected].url); //隐藏加号元素 this.$refs["addImage"].style = "display:none"; //消息框 this.$message({ message: "上传图片成功", type: "success", }); }
-
-
Upload-file这个上传封面组件进行了v-model绑定。
- 一个组件上的
v-model默认会利用名为value的 prop 和名为input的事件,但是像单选框、复选框等类型的输入控件可能会将valueattribute 用于不同的目的。model选项可以用来避免这样的冲突:
- 一个组件上的
-
-
粉丝管理模块是对数据进行简单的渲染,其中粉丝统计用到了echarts,具体操作看echarts的文档。
-
个人设置模块则获取到用户信息进行初始化渲染,并且使用到了图片裁切,运用的是cropper.js,先上传图片直接开始裁切。个人设置的上传图片稍有不同。
-
通过的是label for 和Input 的id属性建立联系
-
<label for="file" class="avatar-wrap"> <el-avatar :size="150" fit="cover" :src="user.photo"></el-avatar> <el-tag class="avatar-title">点击修改头像</el-tag> </label> <input type="file" hidden id="file" ref="imgInput" @change="onUploadChange" />
-
-
cropper.js裁切安装及使用过程
-
先安装
-
npm install cropperjs
-
-
再导入css和裁切
-
import "cropperjs/dist/cropper.css"; import Cropper from "cropperjs";
-
-
最后在打开dialog弹出框的时候初始化裁切器,在关闭的时候销毁裁切器。如果需要在组件初始化就进行初始化裁切器的话,则需要在mounted函数中。
-
//弹出框确定提交 onSuccessSumbit() { //开启成功的加载状态 this.sucessLoading = true; this.cropper.getCroppedCanvas().toBlob(async (file) => { const fd = new FormData(); fd.append("photo", file); //发送请求 await updateUserPhoto(fd); //关闭对话框 this.imgDialogVisible = false; this.$message({ message: "更新头像成功", type: "success", }); //更改用户地址 this.user.photo = window.URL.createObjectURL(file); //通过事件总线存储这个预览图片 globalBus.$emit("updateImg", this.previewImage); //关闭加载状态 this.sucessLoading = false; }); }, -
在个人设置页面提交图片成功后头像成功更改,但是顶部的头像和名字不会更改,因为是非父子组件,跨级组件之间则需要事件总线来解决问题。
-
先封装一个总线js文件并导出
-
import Vue from "vue"; export default new Vue();
-
-
在需要通信的组件互相导入
-
import globalBus from "@/utils/global-bus";
-
-
A向B组件发射事件,比如这里个人设置页面发送给layout组件
-
globalBus.$emit("updateName", res.data.name); globalBus.$emit("updateImg", this.previewImage);
-
-
B组件进行注册,接受一个回调函数
-
//更新名字 globalBus.$on("updateName", (name) => { this.user.name = name; }); //更新图片 globalBus.$on("updateImg", (img) => { this.user.photo = img; });
-
-
-
-
-
-
最近对项目进行了一些简单的打包优化处理
-
所谓的优化主要涉及到两方面:
- 构建速度的优化
- 构建质量的优化
-
Gzip 压缩
- Gzip 压缩是一种数据传输过程中的压缩方式,它可以极大的压缩文件的大小。注意:它不影响原始文件。
- 网站加载的速度很大程序取决于网站资源文件的大小,减少要传输的文件的大小可以使网站不仅加载更快,而且对于那些宽带是按量计费的用户来说也更友好。
- HTTP协议上的GZIP编码是一种用来改进WEB应用程序性能的技术。
- 好处:Gzip 开启以后会将输出到用户浏览器的数据进行压缩的处理,这样就会减小通过网络传输的数据量,提高文件传输的速度。
- 备注:一般是部署的时候设置,比如:nginx、Tomcat…
-
不打包第三方包
-
作用:提高编译的速度。
-
影响打包速度最根本的原因就是某些第三方包体积过大,所以打包速度就很慢。解决它的办法也非常简单:不对它打包!不要让 webpack 来处理它。可是不处理它,把它放哪里呢?通过
script标签加载它,就不被 webpack 处理了。我们推荐使用第三方的 CDN 来加载资源,所谓的 CDN 说白了就是一个在线链接。-
bootcdn:不错,国内的一个服务
-
cdnjs:不推荐,国外的,速度慢
-
unpkg:不推荐,国外的,速度慢
-
jsdelivr :国外的,但是在国内有它的服务节点,推荐
-
- 全球 CDN 优化
-
- 凡是能通过 npm 下载的包,它里面都有
-
-
但是通过 script 标签加载的资源如何使用呢?那就得做一些特殊配置了。
-
在项目的根目录创建
vue.config.js-
/** * Vue CLI 的配置文件 * 这里可以自定义 VueCLI 内部的 webpack 配置 */ // 该配置文件必须导出一个对象(Node 中的模块语法) module.exports = { // 自定义 VueCLI 中的 webpack 配置 configureWebpack: { // 告诉 webpack 使用 script 标签加载的那个资源,而不是去 node_moudles 中打包处理 externals: { // 属性名:你加载的那个包名 // 属性值:script 标签暴露的全局变量,注意,写到字符串中!!! // 'element-ui': 'ELEMENT' 'vue': 'Vue', 'element-ui': 'ELEMENT', 'echarts': 'echarts' } } }
-
-
-
-
最后进行打包测试
-
进行路由懒加载
-
按需加载第三方组件
-
缓存和并行处理
-
VueCLI 内置了:
-
cache-loader会默认为 Vue/Babel/TypeScript 编译开启。文件会缓存在node_modules/.cache中——如果你遇到了编译方面的问题,记得先删掉缓存目录之后再试试看。 -
thread-loader会在多核 CPU 的机器上为 Babel/TypeScript 转译开启
-
-
如果你没有使用 VueCLI,自己搭建的 webpak,建议加上这两个工具优化构建速度。
-
-
-
补充一下:功能优化
-
在客户端造成的一系列响应码的处理以及有人非正常登录的处理
-
在axios响应拦截器中处理
-
request.interceptors.response.use( function(response) { // 在接收响应做些什么,例如跳转到登录页 return response; }, function(error) { // 对响应错误做点什么 if (error.response && error.response.status === 401) { //清除本地伪造的user removeItem("user"); //跳转到登录页 router.push("/login"); Message.error("登录状态无效,请重新登录"); } else if (error.response.status === 403) { Message.error({ type: "warning", message: "没有操作权限", }); } else if (error.response.status === 400) { Message.error("请求参数错误"); } else if (error.response.status >= 500) { Message.error("服务器内容错误,请稍后再试"); } return Promise.reject(error); } );
-
-

浙公网安备 33010602011771号