记一次项目总结

前言

前段时间作了一个页面,作的是我的云盘的业务,操做功能上相似于百度网盘和windows文件管理。这个业务自己没有称得上是亮点的地方,可是当中有不少地方值得总结,不管是技术上仍是感悟上。javascript

个人感悟首先在产品上,做为一名前端,要不断地站在用户的角度上去感觉它,必定有一些能够作的更友好、更人性化的地方。好比在移动复制文件/文件夹的操做中,原来只能经过右键菜单操做,如今能够经过键盘ctrl + vc/x/v,也能够直接拖动(移动)。css

其次在本次编码中,我有如下意识和习惯:html

  • 代码的解耦(合理拆分:分为函数、组件/类、文件三个维度上的解耦)
  • 当前技术栈下的代码可优化点和优雅、正确的编程方式
  • 代码的复用性和可扩展性
  • 过程记录、过后总结、API文档书写

而后,还有几个感悟:前端

  1. 当使用新的标准API、开源项目时,要先进行考察。考察点除了功能上可否知足外,还要着重当作熟度与活跃度,更重要的是要看它的问题列表,有没有获得足够的、及时地解决和回复。
  2. 对于某些具体的技术问题,只要肯思考、敢啃硬骨头,大部分问题都是能解决的
  3. 做为前端从业人员,位于数据链的最下游,受制于后端人员的时间和精力等因素,很容易受影响拖慢开发进度,因此最好仍是要拓宽本身的技术栈

最后,还有一些收获:java

  • 学习到的具体的技术点若干
  • 技术解决方案若干
  • 公共组件、公共方法的开发经验(开始尝试造轮子)

接下来,我把全部相关的技术点整理在这里,巩固学习。清单以下:node

  • 技术点
    • HTML5 Observer API
    • React props派生
    • 滚动事件和滚轮事件
    • 事件委托的原生封装
    • 在线图片转化为base64编码
    • 浏览器(内核)及版本的判断
    • 兼容Linux、Windows、Mac的文件命名规则的方案
    • React组件的props控制(破坏性魔改)
    • IE中使用base64报错“传递给系统调用的数据区域太少”的问题
  • 技术方案
    • HTML5拖拽API的兼容性处理方案
    • web大文件分片上传和断点续传的实现(只有思路,没有成熟方案)
    • 下载异常、错误的友好提示处理
    • 多行文本省略号效果,在系统字体可变化的状况下,可以合理展现的解决方案
  • 关于公共组件
    • 何时须要公共组件
    • 公共组件的做用、特色
    • 内部的运做方式(公共组件与外界的交流方式、内部的状态管理)
    • 公共组件是如何在业务中实现功能的

技术点

HTML5 Oberver API

HTML5 增长了一批 Oberver API ,包括 MutationObserver, PerformanceObserver, IntersectionOberver, ResizeObserver 等,目的是针对一些目标进行监控。这些 API 中只有 MutationObserver (针对DOM结构的监控)进入了正式标准,PerformaneObserver 进入候选阶段,IntersectionObserver 和 ResizeObserver 目前在草案阶段。因此这里讲解一下 MutationObserver,它有一个构造函数 MutationObserver() 和 三个方法 disconnect()、observe()、takeRecords()python

MutationObserver(callback)    构造函数,返回一个监听DOM变化的MutationObserver对象
   回调函数:当指定的被监控DOM节点发生变更时执行。有两个参数:第一个是 MutationRecord 对象数组,即描述全部被触发改动的对象,详细的变更信息存储在这些对象中。第二个是当前调用该函数的 mutationObserver 对象

.observe(target, opinions)    开始监控DOM节点
    target是被监控的DOM节点
    opinions可选,是一个对象,属性有:
        attributeFilter     要监控的DOM属性,若无此属性,默认监控全部属性。无默认值
        attributeOldValue     当被监控节点的属性改动时,将次属性置为true将记录任何有改动属性的上一个值。无默认值
        attributes        置为true以观察受监视元素的属性值变动。默认值为false
        characterData  置为true以观察受监视元素的属性值变动。默认值为false
        characterDataOldValue  置为true以在文本在受监视节点上发生更改时记录节点文本的先前值。
        childList            置为true以监视目标节点(若是subtree为true,则包含子孙节点)添加或删除新的子节点。默认值为false。
        subtree            置为true以扩展监视范围到目标节点下的整个子树的全部节点。MutationObserverInit的其余值都会做用于此子树下的全部节点,而不只仅只做用于目标节点。默认值为false。
    
.disconnect()        此方法告诉观察者中止监控

.takeRecords()      此方法返回已检测到但还没有由观察者的回调函数处理的全部匹配DOM更改的列表,使变动队列保持为空。 此方法最多见的使用场景是在断开观察者以前当即获取全部未处理的更改记录,以便在中止观察者时能够处理任何未处理的更改。

React props 派生

何为 props 派生?好比如今有这样的需求,子组件中来自父组件的 props 数据,并非直接使用,而是在其基础上进行更改事后才会使用,所以须要 props 变化时更新 state 的操做,能够经过生命周期函数实现。react

在react16.4版本以前经过 componentWillReceiveProps 来实现,16.4以后还能够经过 getDerivedStateFromProps 来实现。另外,在具体状况下是否真的须要 props 派生、注意事项及可能出现的bug官网博客总结的很详细nginx

你可能不须要派生stateweb

滚动事件和滚轮事件

滚动事件 onscroll,滚轮事件 onwheel。在PC端通常容易被认为没什么区别,但仍是有些细微的差异。不管经过何种方式(鼠标滚轮、键盘方向键、触摸板)滚动页面,只要有滚动发生都会触发滚动事件。而滚轮事件不管页面有无发生滚动,只要滚轮被触动,都会发生该事件。大部分时候只须要滚动事件便可,个别时候滚轮事件配合使用。好比想页面已经滚动到底部,仍在滚动滑轮时,只发生滚轮事件不发生滚动事件,有这个需求能够配合使用。注意,滚轮事件要使用onwheel,onmousewheel已被废弃。

事件委托的原生封装

在封装事件委托以前,有几个问题须要明白:

为何须要事件委托?

  1. 提升页面性能
  2. 有时候想要为某元素绑定监听事件,但没法获取其DOM元素,这个时候能够获取其祖先元素,利用事件委托便可绑定事件监听

jQuery不是有 on() 方法来实现事件委托吗?为何还要本身封装?

进入React、Vue、Angular的前端组件化 + 前端工程化时代,咱们应该改变思惟,在开发中尽可能不要使用jQuery。你应该首选使用React提供的事件处理机制,尽可能不要使用原生JS处理事件。当你确认React的事件处理没法知足你的需求、或者不方便实现时,可使用addEventListener()。虽然这里封装了 onDelegate(),但仍是建议你不在万不得已的状况下不要使用。

使用文档

实现源码:

import cloneDeep from "lodash/cloneDeep";

const throwError = (message) => { throw new Error(message) };

// 判断是不是DOM元素
const isDOM = (obj) => typeof HTMLElement === 'object' ?
      obj instanceof HTMLElement
      :
      obj && typeof obj === 'object' && obj.nodeType === 1 && typeof obj.nodeName === 'string';

// 检查selector的有效性
const checkSelector = (parent, selector) => {
   try{
      parent.querySelector(selector);
   }catch (e) {
      return `参数 selector 无效`
   }
};


// 参数检测
const paramCheck = (type, events, parent, selector, func, data, reverseScope, capture) => {
   let baseMsg = `Document模块 ${type}Delegate()方法调用错误:`;

   if (type === "on")
   {
      typeof events !== "string" && throwError(`${baseMsg}参数 events 必须是 string 类型,如今是${typeof events}!`);
      events.length === 0 && throwError(`${baseMsg}参数 events 不能为空!`);
      !isDOM(parent) && throwError(`${baseMsg}参数 parent 必须是 DOM 元素!`);
      typeof selector !== "string" && throwError(`${baseMsg}参数 selector 必须是 string 类型,如今是${typeof selector}!`);
      let selectRes = checkSelector(parent, selector); // 检测selector的有效性
      typeof selectRes === "string" && throwError(`${baseMsg}${selectRes}!`);
      typeof func !== "function" && throwError(`${baseMsg}参数 func 必须是 function 类型,如今是${typeof func}!`);
      typeof reverseScope !== "boolean" && throwError(`${baseMsg}参数 reverseScope 必须是 boolean 类型,如今是${typeof reverseScope}!`);
      typeof capture !== "boolean" && throwError(`${baseMsg}参数 capture 必须是 boolean 类型,如今是${typeof capture}!`);
      Object.prototype.toString.call(data).slice(8, -1) !== "Object" && throwError(`${baseMsg}参数 data 必须是 object 类型!`); // 判断data数据类型
   }else if(type === "off")
   {
      typeof events !== "string" && throwError(`${baseMsg}参数 events 必须是 string 类型,如今是${typeof events}!`);
      events.length === 0 && throwError(`${baseMsg}参数 events 不能为空!`);
      let selectRes = checkSelector(parent, selector); // 检测selector的有效性
      typeof selectRes === "string" && throwError(`${baseMsg}${selectRes}!`);
      !isDOM(parent) && throwError(`${baseMsg}参数 parent 必须是 DOM元素!`);
      typeof selector !== "string" && throwError(`${baseMsg}参数 selector 必须是 string 类型,如今是${typeof selector}!`);
      typeof func !== "function" && throwError(`${baseMsg}参数 func 必须是 function 类型,如今是${typeof func}!`);
      typeof reverseScope !== "boolean" && throwError(`${baseMsg}参数 reverseScope 必须是 boolean 类型,如今是${typeof reverseScope}!`);
   }
};


let EventHandles = [];

// 事件委托
const onDelegate = (events = "", parent, selector = "",  func, data = {}, reverseScope = false, capture = false) => {
   data = cloneDeep(data);
   paramCheck("on", events, parent, selector, func, data, reverseScope, capture);  // 参数检测

   const already = EventHandles.find(f => f.events === events && f.parent === parent && f.selector === selector && f.func === func && f.reverseScope === reverseScope);
   if(!already)
   {
      const handler = (e) => {
         let flag = false, target = e.target, selectList = Array.from(parent.querySelectorAll(selector));
         while (target.tagName !== "BODY")
         {
            if (selectList.includes(target))
            {
               let event = { delegateTarget: parent, currentTarget: target, data: data, originalEvent: e };
               !reverseScope && func(event);
               flag = true;
               break;
            }
            target = target.parentNode ? target.parentNode : "";
         }
         let event = { delegateTarget: parent, currentTarget: e.target, data, originalEvent: e };
         reverseScope && !flag && func(event);
      };
      parent.addEventListener(events, handler, capture);
      EventHandles.push({ events, parent, selector, func, reverseScope, handler });
   }
};

// 解除由onDelegate()绑定的事件监听
const offDelegate = (events = "", parent, selector = "", func, reverseScope = false) => {
   paramCheck("off", events, parent, selector, func, {}, reverseScope);
   let hands = EventHandles.filter(f => f.events === events && f.parent === parent && f.selector === selector && f.func === func && f.reverseScope === reverseScope);
   hands.forEach(i => {
      parent.removeEventListener(events, i.handler);
      EventHandles.splice(EventHandles.indexOf(i), 1);
   });
};


export { onDelegate, offDelegate };

在线图片转化为base64编码

这个需求可能不太常见

export const convertImgUrlToBase64 = (url, outputFormat) => new Promise((resolve, reject) => {
   let img = document.createElement("img"); img.crossOrigin = 'Anonymous';
   let canvas = document.createElement('CANVAS');
   let ctx = canvas.getContext('2d');
   img.src = url;
   img.addEventListener("load", () => {
      canvas.height = img.height;
      canvas.width = img.width;
      ctx.drawImage(img, 0, 0);
      let dataURL = canvas.toDataURL(outputFormat || 'image/png');
      resolve(dataURL);
      canvas = null;
   });
});

浏览器内核(版本)的判断

这里判断浏览器外壳没有太大的意义,重要的是判断内核。

export const judgeBrowserType = () => {
   const agent = navigator.userAgent;
   let browser = "", version = "-1", ver;
   switch (true) {
      case agent.includes("Opera"):   // Opera浏览器(非Chromium内核, 老版本)
         browser = "opera"; ver = agent.match(/Version\/([\d.]+)/)[1].split("."); version = `${ver[0]}.${ver[1]}`;
         break;
      case agent.includes("Trident") || agent.includes("MSIE"): // IE浏览器 或 IE内核
         browser = "ie";
         agent.includes("MSIE") && (ver = agent.match(/MSIE\/([\d.]+)/)[1].split("."));
         !agent.includes("MSIE") && (ver = ["11", "0"]);
         version = `${ver[0]}.${ver[1]}`;
         break;
      case agent.includes("Edge"):    // Edge浏览器
         browser = "edge"; ver = agent.match(/Edge\/([\d.]+)/)[1].split("."); version = `${ver[0]}.${ver[1]}`;
         break;
      case agent.includes("Firefox"): // Firefox浏览器
         browser = "firefox"; ver = agent.match(/Firefox\/([\d.]+)/)[1].split("."); version = `${ver[0]}.${ver[1]}`;
         break;
      case agent.includes("Gecko") && !agent.includes("like Gecko"): // 非Firefox的Gecko内核, 没法判断版本
         browser = "firefox";
         break;
      case agent.includes("Safari") && !agent.includes("Chrome"):    // Safari浏览器
         browser = "safari"; ver = agent.match(/Version\/([\d.]+)/)[1].split("."); version = `${ver[0]}.${ver[1]}`;
         break;
      case agent.includes("Chrome") && agent.includes("Safari"):      // Google Chrome 或 Chromium内核
         browser = "chrome"; ver = agent.match(/Chrome\/([\d.]+)/)[1].split("."); version = `${ver[0]}.${ver[1]}`;
         break;
      default:
         browser = "others";
         break;
   }
   return { browser, version }
};

兼容Windows、Mac、Linux的命名规则

各平台的文件命名规则:

  • Windows
    • 不能超过 255 个字符(含扩展名)或 127 个中文字符
    • 文件名能够包含除 ? " " / \ < > * | : 以外的大多数字符
    • 除了开头以外任何地方均可以使用空格
    • 保留大小写格式,但不区分大小写
  • Mac
    • 不能包含冒号 : 不能以句点 . 开头
    • 部分 APP 可能不容许使用斜杠 / 
  • Linux
    • 大小写敏感
    • 不容许使用 / 
    • 不容许将 . 和 .. 当作文件名
// turn === "turn"表示,不合规的字符会被修改为下划线(超出长度的字符被剪掉)
const nameRule_compatible = (fullName, name, turn) => {
   let flag = true;
   let errorMsg = "";
   const forbid = `?"/\\<>*|:`;

   fullName = fullName.trim(); name = name.trimLeft();

   if(fullName.length > 255)
   {
      errorMsg = getLabel(513983, '名称不得超过255个字符');
      if (turn === "turn")
      {
         name = name.substring(0, 216);
         fullName = `${name}.${fullName.split(".").pop()}`;
      }
   }

   for(let i=0; i<forbid.length; i++)
   {
      if(name.includes(forbid[i]))
      {
         errorMsg = `${getLabel(513984, '文件名不能包含下列任何字符')}: \\ / : * ? " < > | `;
         if (turn === "turn")
         {
            let regExp = new RegExp(`\\${forbid[i]}`, "g");
            name = name.replace(regExp, "_");
            fullName = fullName.replace(regExp, "_");
         }
      }
   }

   if(name[0] === ".")
   {
      errorMsg = getLabel(513985, '不能以 . 开头');
      if (turn === "turn")
      {
         fullName = "_" + fullName.substring(1);
         name = "_" + name.substring(1);
      }
   }

   if(errorMsg)
   {
      flag = false;
      message.warn(errorMsg);
   }

   return [flag, fullName, name]
};


export {
   nameRule_compatible,
}

React 组件的 props 使用

这里”组件 props 使用“指的是:当使用某个组件时,没法直接接触到其内部使用的某组件,而这时但愿改变该某组件的 props 传参。这里有两个方法,一是获取目标组件的ref,能够直接修改值;二是直接获取目标组件的变量(或其父组件、祖先组件,能够顺着找到目标组件)来操做。须要指出的是,第二种方法具备破坏性,能够在实在没办法的状况下使用。

IE 中使用 base64 时报错

报错“传递给系统调用的数据区域过小”。是因为 IE 浏览器中对 href、src 这些属性对 url 长度有限制,而 base64 通常都比较长。原理上讲,先将 base64 转 blob 再生成 url,但由 blob 生成 url 这部分操做(HTML标准)的结果,在IE下会报错。怎么解决呢?使用IE本身的API: window.navigator.msSaveOrOpenBlob(blob, fileName);

技术方案整理

HTML5 拖拽 API 的兼容处理方案(除了拖拽上传)

本业务中的功能是:拖拽文件图标至文件夹中,完成文件移动的功能。此功能在开发中依照 HTML5 标准 API 编写,基于最新的稳定版Google Chrome(78),并未发现任何兼容性问题。

1.Firefox 存在打开新标签页的问题:拖拽释放在目标元素上时会打开新标签页

  解决方法:drop 事件中阻止默认行为

2.IE 某些版本有兼容性问题:

  IE11:在 dataTransfer.setData() 时,键不能自定义,只能是标准规定的如 Text

  IE十、IE9:支持 HTML5 标准,但本人未做测试

  IE9 如下:不支持标准 API

3.Edge:

  旧版本的 KTHML 内核:测试版本是16,遇到的问题与 IE11 相同,处理方式相同

  新版本的 Chromium 内核:无兼容性问题

4.国产浏览器拖拽释放会打开新标签页:

  IE 内核:不要使用 dataTransfer 对象来传递数据,可使用共享的变量(如全局变量、store、类属性this.xxx),须要该数据去维护

  Chromium 内核:缘由在于 e.dataTransfer.setData() 中的 key (貌似须要使用自定义key)

5.父元素容许拖拽时,子元素想要被选中文本(子元素自动被容许拖拽):

  若是子元素是 input,子元素 draggable=true,dragstart 事件阻止默认事件便可

  若是子元素是普通元素,使用 mousedown/mouseup 事件 或 mouseenter/mouseleave事件 相互配合,改变父元素的 draggable 属性

6.拖动元素在目标元素上晃悠,目标容器元素表现异常:

  指望效果是在 dragenter 事件(进入目标元素时)改变背景颜色,dragleave 事件(离开目标元素时)恢复背景颜色。现实状况是:进入目标元素后离开目标元素前不断闪烁(屡次交替发生 dragenter/dragleave),而且时长没法恢复背景颜色。

  缘由:若是目标元素内部没有子元素,不会出现上述异常。若是内部有多个子元素(及后代元素),那么拖动元素在目标元素上通过子元素时会有上述异常。明明是在目标元素上绑定的这两个事件,却在其全部的后代元素上都会触发(并不是冒泡)

  解决方法:设置一个缓存变量(布尔值),标记当前是否进入/离开目标元素,排除子元素的干扰,便可。

7.拖拽下载:只有 Chrome 支持,暂没测试 Chromium 内核其它浏览器

只需将 dataTransfer 对象设置 DownloadURL 便可。

Web大文件分片上传和断点续传(没有具体方案,但有总体思路)

断点续传必然要分片上传,前端将文件分片上传,后端一个一个地接收分片并存储,当所有接收完毕后再合并。所以,在分片上传时,须要先后端协商好文件名、任务ID、分片个数、当前分片索引等信息。

分片上传建议一个一个地上传(串行上传),当用户暂停上传时,当前正在上传的分片中断,下次继续上传时,今后分片开始上传。

前端的核心问题是如何实现文件分片,后端的核心问题是如何将文件合并、什么时候合并。

前端分片经过 HTML5 FILE API 的 slice() 方法,可将文件分片。后端在所有分片接收完毕时便可开始合并,合并思路:新建二进制文件,按顺序读取分片,将读取的二进制流依次写入新文件,正确命名特别是扩展名,便可完成合并。

前端实验代码:

function SliceUploadFile() {
  let fileObj = document.getElementById("file").files[0];  // js 获取文件对象

   const itemSize = 8 * 1024 * 1024;    // 分片大小:8M
   const number = Math.ceil(fileObj.size / itemSize);   // 分片数量
  let prev = 0;
  for(let i=0; i<number; i++)
  {
     let start = prev;
     let end = start + itemSize;
     let blob = fileObj.slice(start, end);
     let msg = {type: "slice", name: fileObj.name, task: "fileTest", count: number, current: i};
     // FormData 对象
      var form = new FormData();
      form.append("author", "xueba");             // 能够增长表单数据
     {#console.log("msg", msg);#}
      form.append("msg", JSON.stringify(msg));
      form.append("file", blob);// 文件对象

      // jQuery ajax
      $.ajax({
         url: "/upload/",
         type: "POST",
         async: true,      // 异步上传
         data: form,
         contentType: false, // 必须false才会自动加上正确的Content-Type
         processData: false, // 必须false才会避开jQuery对 formdata 的默认处理。XMLHttpRequest会对 formdata 进行正确的处理
         xhr: function () {
            let xhr = $.ajaxSettings.xhr();
            xhr.upload.addEventListener("progress", progressSFunction, false);
            xhr.upload.onloadstart = (e) => {
               progress[0] = {
                 last_laoded: 0,
                 last_time: e.timeStamp,
              };
               console.log("开始上传",progress);
             };
            xhr.upload.onloadend = () => {
               delete progress[0];
               console.log("结束上传",progress);
             };
            return xhr;
         },
         success: function (data) {
            data = JSON.parse(data);
            data.forEach((i) => {
               console.log(i.code, i.file_url);
             });
         },
         error: function () {
            alert("aaa上传失败!");
         },
       });
      prev = end
  }
}

后端分片上传代码:

        try:
            resList, fileList = [], request.FILES.getlist("file")
            msg = json.loads(request.POST.get("msg"))
            print(f"msg: {msg['type']}, count: {msg['count']}, current: {msg['current']}")

            dir_path = 'static/files/{0}/{1}/{2}'.format(time.strftime("%Y"), time.strftime("%m"), time.strftime("%d"))
            if os.path.exists(dir_path) is False:
                os.makedirs(dir_path)
            for file in fileList:
                filename = f"{msg['current']}_{msg['task']}" if msg['type'] == "slice" else file.name
                file_path = '%s/%s' % (dir_path, filename)
                file_url = '/%s/%s' % (dir_path, filename)
                res = {"code": 0, "file_url": ""}
                with open(file_path, 'wb') as f:
                    if f == False:
                        res['code'] = 1
                    for chunk in file.chunks():  # chunks()代替read(),若是文件很大,能够保证不会拖慢系统内存
                        f.write(chunk)
                res['file_url'] = file_url
                resList.append(res)
            return HttpResponse(json.dumps(resList))
        except:
            return HttpResponse("error")

后端分片合并代码:

def mergeFiles():
    "合并分片文件"
    path = "../static/files/2019/11/26"

    fileList = [file for file in os.listdir(path)]
    fileList.sort(key=lambda x: int(x.split("_")[0]))
    maxIndex = int(fileList[-1].split("_")[0])
    mergeName = "企业应用-部署介绍和nginx安装.mp4"

    with open(f"{path}/{mergeName}", "wb") as f:
        for fileName in fileList:
            print("正在合并", fileName)
            with open(f"{path}/{fileName}", "rb") as file:
                # f.write(file.read())
                for line in file:
                    f.write(line)
下载异常、错误时的友好提示方案

在整个系统中常见文件下载,下载自己的实现也很简单,但下载若是有异常可能会致使前端页面报错、白屏、错误页等问题,也就是提示不友好的问题。

  • 当使用 window.location.href = "" 时,一旦下载异常,页面立马坏掉
  • 当使用 <a href="" download=""> 时,下载异常,页面不会有问题,但会下载一个无效文件,且没法提示用户,会让人感受错愕
  • 当使用 iframe 下载,能够监控 iframe 的 onload 事件,下载异常子页面会坏掉,主页面没有问题,但也没法提示用户,看起来没有反应

通过思考,我认为这须要先后端的配合才能够作到友好提示,以下:

  • 首先对下载地址发送 HEAD 请求,探测应用层面是否能走通,若是返回状态码 200 说明网络是没有问题的,开始下载
  • 在 iframe 中的 a 标签开始下载(不要 download 属性),若是下载发生异常,首先排除网络问题,能够肯定是服务端有错误。这时须要服务端作异常处理,捕获异常后响应给前端,返回提示字符串
  • 前端接收到字符串,会将字符串直接呈如今 iframe 中,能够经过 onload 事件监控到,将内容读取能够呈现给用户
  • 若是下载过程当中出现网络异常,浏览器会自动处理(中断下载),页面不会有问题
多行文本省略号效果在系统字体变化的状况下可以合理展现的解决方案

多行文本省略号,目前 CSS 没有正式的标准方案,webkit 内核的浏览器(Chromium内核【Chrome、Edge、Opera、国产浏览器】、Firefox68+、Safari)有非标准的 CSS 方案能够实现。可是对于低版本火狐、旧版Edge、IE、旧版Opera等,没法只经过 CSS 实现。之前的处理办法是 overflow: hidden,再设置 max-height、固定 width。虽然没有省略号效果,可是也能看得过去。

如今的状况不一样了,系统的字体能够随时变化:“大”、“中”、“默认”。致使在 overflow: hidden 时,max-height 的值没法固定,此方案行不通。所以,在这种状况下,通过个人摸索找到了两种方法:

  • 通过研究发现,切换系统字体时,实际上是切换了一套 css 文件,经过 MutationObserver 能够监控 <head> 中 <style> 的变化,能够获知当前用了多大的字体,而后采用对应准备好的 CSS 类。此方法需考虑浏览器的 Observer API 兼容性问题
  • 截取文本的字节长度,超出指定长度后截取并加上“...”,此方法不存在浏览器兼容问题

这里重点讲第一种方法的 CSS(LESS) ,能够作到在对应的系统字体下,3行之内没有省略号,超过3行出现省略号:

LESS:

.WeaDoc-showName{
        color: #333333;
        margin-top: 6px;
        letter-spacing: -0.08px;
        display: inline-block;
        position: relative;
        word-wrap: break-word;
        word-break: break-all;
        cursor: text;
        min-width: 25px;

        .textname{
            position: relative;
            overflow: hidden;
            text-overflow: ellipsis;
            display: -moz-box;
            display: -webkit-box;
            -ms-box-orient: vertical;
            -moz-box-orient: vertical;
            -webkit-box-orient: vertical;
            -ms-line-clamp: 2;
            -moz-line-clamp: 2;
            -webkit-line-clamp: 2;
        }

        @font-size-list: 12, 14, 16;
        @font12-height: 38px;
        @font14-height: 44px;
        @font16-height: 51.2px;
        @gradient-color: white;
        @base-after-number: 7;
        .show-name-common(@height){
            float: right;
            width: 100%;
            margin-left: -5px;
            max-height: @height + 1;
        }
        .show-before-common(@height){
            content: "";
            float: left;
            width: 5px;
            height: @height;
        }
        .show-after-common(@bottom, @fontSize, @backColor: white){
            content: "...";
            float: right;
            position: relative;
            bottom: ~"@{bottom}px";
            left: 100%;
            width: 30px;
            font-size: ~"@{fontSize}px";
            margin-left: -30px;
            padding-right: 5px;
            background: linear-gradient(to right, transparent, @backColor 45%, @backColor);
            box-sizing: content-box;
            text-align: right;
            transform: translateX(-4px);
            pointer-events: none;
        }

        .font-compatible-loop(1, 7);
        .font-compatible-loop(@i, @base) when (@i <= length(@font-size-list)) {
            @size: extract(@font-size-list, @i);
            @heightStr: "font@{size}-height";
            .forLoopItem(@size, @@heightStr, @base);
            .font-compatible-loop(@i + 1, @base + 1);
        }

        .forLoopItem(@size, @height, @base){
            &.text-@{size}{
                height: @height;
                overflow: hidden;
                .textname{
                    .show-name-common(@height: @height);
                }
                &::before{
                    .show-before-common(@height: @height);
                }
                &::after{
                    .show-after-common(@bottom: @size + @base, @fontSize: @size);
                }
            }
        }

    }

变异后的CSS:

.WeaDoc-showName {
  color: #333333;
  margin-top: 6px;
  letter-spacing: -0.08px;
  display: inline-block;
  position: relative;
  word-wrap: break-word;
  word-break: break-all;
  cursor: text;
  min-width: 25px;
}
.WeaDoc-showName .textname {
  position: relative;
  overflow: hidden;
  text-overflow: ellipsis;
  display: -moz-box;
  display: -webkit-box;
  -ms-box-orient: vertical;
  -moz-box-orient: vertical;
  -webkit-box-orient: vertical;
  -ms-line-clamp: 2;
  -moz-line-clamp: 2;
  -webkit-line-clamp: 2;
}
.WeaDoc-showName.text-12 {
  height: 38px;
  overflow: hidden;
}
.WeaDoc-showName.text-12 .textname {
  float: right;
  width: 100%;
  margin-left: -5px;
  max-height: 39px;
}
.WeaDoc-showName.text-12::before {
  content: "";
  float: left;
  width: 5px;
  height: 38px;
}
.WeaDoc-showName.text-12::after {
  content: "...";
  float: right;
  position: relative;
  bottom: 19px;
  left: 100%;
  width: 30px;
  font-size: 12px;
  margin-left: -30px;
  padding-right: 5px;
  background: linear-gradient(to right, transparent, white 45%, white);
  box-sizing: content-box;
  text-align: right;
  transform: translateX(-4px);
  pointer-events: none;
}
.WeaDoc-showName.text-14 {
  height: 44px;
  overflow: hidden;
}
.WeaDoc-showName.text-14 .textname {
  float: right;
  width: 100%;
  margin-left: -5px;
  max-height: 45px;
}
.WeaDoc-showName.text-14::before {
  content: "";
  float: left;
  width: 5px;
  height: 44px;
}
.WeaDoc-showName.text-14::after {
  content: "...";
  float: right;
  position: relative;
  bottom: 22px;
  left: 100%;
  width: 30px;
  font-size: 14px;
  margin-left: -30px;
  padding-right: 5px;
  background: linear-gradient(to right, transparent, white 45%, white);
  box-sizing: content-box;
  text-align: right;
  transform: translateX(-4px);
  pointer-events: none;
}
.WeaDoc-showName.text-16 {
  height: 51.2px;
  overflow: hidden;
}
.WeaDoc-showName.text-16 .textname {
  float: right;
  width: 100%;
  margin-left: -5px;
  max-height: 52.2px;
}
.WeaDoc-showName.text-16::before {
  content: "";
  float: left;
  width: 5px;
  height: 51.2px;
}
.WeaDoc-showName.text-16::after {
  content: "...";
  float: right;
  position: relative;
  bottom: 25px;
  left: 100%;
  width: 30px;
  font-size: 16px;
  margin-left: -30px;
  padding-right: 5px;
  background: linear-gradient(to right, transparent, white 45%, white);
  box-sizing: content-box;
  text-align: right;
  transform: translateX(-4px);
  pointer-events: none;
}

关于公共组件

组件库也属于公共组件的范畴,在业务中被大量复用。通常在大公司中会有本身的一套组件库供业务开发使用,注重通用性、便捷性。但对于一个庞大的系统而言,一套组件库不能照顾到全部的边边角角,有些模块须要定制本身的公共部分、有些部分可能只在这一个模块中被复用。在这里,我说的公共组件指的是这部分。

何时须要公共组件?

首先,你须要一个组件或者一些功能,却并无现成的轮子。(确认组件库中真的没有这部分)

其次,你须要的这个组件,可能会在不少地方复用

公共组件的做用、特色

  • 复用性。同一个功能能够在不少地方被复用,提高程序质量
  • 通用性。能够知足多个业务场景的需求,能够兼顾它们的需求
  • 做为基础设施提供 API。让业务开发者更专一于业务逻辑,而不是各类细节处理(往大了说,全部的框架、库、中间件甚至浏览器和操做系统不都具备这个做用么)

公共组件是如何在业务中实现功能的

功能由谁实现

首先要明白,在 React 中一个功能可能并不彻底由公共组件实现、也有多是公共组件与业务代码相互配合实现(这样的状况不少),所以咱们在开发公共组件的时候要明白,如何权衡、如何划分最合理

  • 如何在 UI 中划分
  • 明确职责,哪些功能是须要由公共组件完成、哪些功能交由业务代码完成、哪些功能最好让二者相互配合

公共组件内部

公共组件内部也要注意合理拆分(解耦),为了代码具有更好的扩展性、可维护性、可读性,分为三个维度的拆分:

  • 函数的拆分
  • 组件的拆分
  • 文件的拆分

状态数据的管理:绝大部分的公共组件内部均可以使用 React 自身的 API 实现状态管理(state、hooks),若是该公共组件过于庞大,内部过于复杂,可使用 mobx、redux 等状态管理。不管哪一种方式,这些状态数据都是只供组件内部使用的,不能是外部使用的。

如何使用公共组件(交互方式)

对于业务组件来讲,最重要的是明确一个公共组件适合在何时用、该如何使用(掌握 API)

属性:大多数的 React 组件功能经过调整属性 props 便可实现,属性的值能够是全部类型 number/string/boolean/function/...... 。这里又分为几种不一样的方式

  • 普通参数:number/string/boolean等值,根据需求调整功能
  • 受控数据:数组是常见形式。将受控数据开放给业务开发,大大提高灵活性,能够知足个性化的需求
  • 回调:function。一些行为/事件的回调,一般公共组件会在这里传递给业务代码一些参数,也能够有返回值

组件实例 ref :若是公共组件想对外提供一些方法以供调用,须要经过 ref 。这里须要说明的是,你须要考虑哪些方法暴露出去、哪些方法不暴露出去。

相关文章
相关标签/搜索