Fork me on GitHub

热点后台发布系统

热点后台发布系统项目总结

业务逻辑思路以及遇到的一些问题

  • 登录通过后端返回的 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 的事件,但是像单选框、复选框等类型的输入控件可能会将 value attribute 用于不同的目的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);
          }
        );
        
posted @ 2021-05-27 09:24  雨梦Coder  阅读(191)  评论(1)    收藏  举报