场景题

511 阅读18分钟

场景问题

优化层面

1.后端一次给你10万条数据,如何优雅展示?

既然一次渲染10万条数据会造成页面加载速度缓慢,那么我们可以不要一次性渲染这么多数据,而是分批次渲染, 比如一次10000条,分10次来完成, 这样或许会对页面的渲染速度有提升。 然而,如果这13次操作在同一个代码执行流程中运行,那似乎不但无法解决糟糕的页面卡顿问题,反而会将代码复杂化。 类似的问题在其它语言最佳的解决方案是使用多线程,JavaScript虽然没有多线程,但是setTimeout和setInterval两个函数却能起到和多线程差不多的效果。 因此,要解决这个问题, 其中的setTimeout便可以大显身手。 setTimeout函数的功能可以看作是在指定时间之后启动一个新的线程来完成任务。

ajax 请求。。。。
​
function loadAll(response) {
    //将10万条数据分组, 每组500条,一共200组
    var groups = group(response);
    for (var i = 0; i < groups.length; i++) {
        //闭包, 保持i值的正确性
        window.setTimeout(function () {
            var group = groups[i];
            var index = i + 1;
            return function () {
                //分批渲染
                loadPart( group, index );
            }
        }(), 1);
    }
}
​
//数据分组函数(每组500条)
function group(data) {
    var result = [];
    var groupItem;
    for (var i = 0; i < data.length; i++) {
        if (i % 500 == 0) {
            groupItem != null && result.push(groupItem);
            groupItem = [];
        }
        groupItem.push(data[i]);
    }
    result.push(groupItem);
    return result;
}
var currIndex = 0;
//加载某一批数据的函数
function loadPart( group, index ) {
    var html = "";
    for (var i = 0; i < group.length; i++) {
        var item = group[i];
        html += "<li>title:" + item.title + index + " content:" + item.content + index + "</li>";
    }
    //保证顺序不错乱
    while (index - currIndex == 1) {
        $("#content").append(html);
        currIndex = index;
    }
}
​
​

问表单页面,数据很多,怎么不卡(我一开始以为是首屏优化,说懒加载,后面面试官说是滑动的时候不卡,面试官说虚拟滚动,这个真没了解到 🤣)

2.如何进行首屏优化提高渲染速度

关于性能优化

详解 CRP

在开始之前,我们需要明白一个原则:性能优化的最终目的是提升用户体验。 简而言之就是让用户感觉这个网站很「快」(至少不慢hh),这里的「快」有两种,一种是「真的快」一种是「觉得快」

  • 「真的快」:可以客观衡量的指标,像网页访问时间、交互响应时间、跳转页面时间
  • 「觉得快」:用户主观感知的性能,通过视觉引导等手段转移用户对等待时间的关注

对症下药

我们知道是app.js文件太大,加载时间太长导致了首屏加载速度过慢,我们就需要对症下药减小app.js的大小,提高网站访问速度。

一、压缩:

对代码进行压缩,我们可以减小代码的体积量。

二、路由懒加载:

当我们使用路由懒加载后,项目就会进行按需加载,其原理就是利用webpack大法的code splitting,当你使用路由加载的写法,webpack就会对app.js进行代码分割,减小app.js的体积,从而提高首屏加载数点。

没使用路由懒加载前的app.js:

2444cb58e449ec5ade0be219bbc50d11.jpg

使用路由懒加载后对app.js进行code splitting:

859c4c1052f2ec4ce75acad28a040bed.jpg

三、CDN引入:

采用CDN引入,在index.html使用CDN引入,并在webpack配置。打包之后webpack进会从外部打包第三方引入的库,减小app.js的体积,从而提高首屏加载速度。

企业微信截图_16445727114999.png

image.png

没使用CDN引入前app.js的大小:

image.png

使用CDN引入后app.js的大小:

企业微信截图_164273430576.png

四、SSR服务器渲染:

有局限性,禁用了beforeCreate()和created()之外的其他生命周期,我自己没有亲自测试过,但这是一种方案。

五、增加带宽:

增加带宽可以提高资源的访问速度,从而提高首批的加载速度,我司项目带宽由2M升级到5M,效果明显。

六、提取第三方库 vendor:

这是也是webpack大法的code splitting,提取一些第三方的库,从而减小app.js的大小。

代码层面做好懒加载,网络层面把CDN、本地缓存用好,前端页面问题基本解决一大半了。剩下主要就是接口层面和“视觉上的快”的优化了,骨架屏先搞起,渲染一个“假页面”占位;接口该合并的合并,该拆分的拆分,如果是可滚动的长页面,就分批次请求

3.长列表渲染

百万PV商城实践系列

Vue 超长列表渲染性能优化实战

image.png

为什么选择分页+虚拟列表这个方案呢?

首先,我们将每个方案可以解决的问题不能解决的问题做一个梳理,具体的优缺点如下:

  • 分页加载:解决了数据过多问题,通过数据分页的方式减少了首次页面加载的数据和DOM数量。是现今绝大部分的应用都会采用的实施手段。随着页面浏览的页面数据增多,DOM数量也越来越多,还是会存在部分问题。
  • 分片加载:与分页加载相同,只是将用户触底行为获取最新数据的时间节点在一开始进行了切片加载,优先显示页面数据在加载其他数据。会出现页面阻塞和性能问题
  • 虚拟列表:将驱动交给数据,通过区间来直接渲染区间内容中的数据DOM,解决了页面列表内元素过多操作卡顿的问题, 与数据加载无挂钩。

当列举了三种常见的方式后,我们发现单一的方案很难满足我们的诉求。因此,我选择使用分页的方式处理数据加载,同时将渲染页面的事情交给虚拟列表进行渲染。通过结合两种不同侧重点的方案,来满足我们初步的诉求。

通过下面的示意图,我们将整体列表划分为滚动窗口可视窗口。左边是真实的列表,所有的列表项都是真实的DOM元素,而虚拟列表从图中可以看到,只有出现在可视窗口内的列表项才是真实的DOM元素,而未出现在可视窗口中的元素则只是虚拟数据,并未加载到页面上。

与真实列表不同的是,虚拟列表的滚动都是通过transform或者是marginTop做的偏移量,本身列表中只显示视窗区的DOM元素。

image.png

下面,我们就来从0到1实现一个基本的虚拟列表吧。

基本布局

如下结构图,我们先分析下基本页面构成:

  • 第一层为容器层,选定一个固定高度,也就是我们说的可视化窗口
  • 第二层为内容层,一般在这里撑开高度,使容器形成scroll
  • 第三层为子内容层,居于内容层内部,也就是列表中的列表项。
  • ......

image.png

分析后,我将结构图中代码使用JSX实现后,就是下面这个简单的结构:

页面布局代码
<div>
  <div>
    ... List Item Element
  </div>
</div>;
​
.App {
    font-family: sans-serif;
    text-align: center;
}
​
.showElement {
    display: flex;
    justify-content: center;
    align-items: center;
    border: 1px solid #000;
    margin-bottom: 8px;
    border-radius: 4px;
}

先搭建一个简单的页面,然后通过currentViewList渲染出对应的列表项内容。

初始化页面

当我们确定了页面的基本结构后,我们再来完善一些布局与配置,实现一个真实渲染上千条数据的列表。

我先定义了一些配置,包含容器高度、列表项高度、预加载偏移数量等需要用到的固定内容。

  • 容器高度:当前虚拟列表的高度
  • 列表项高度: 列表项的高度
  • 预加载偏移:可视窗上下做预加载时需要额外展示几个预备内容
页面属性
/** @name 页面容器高度 */
​
const SCROLL_VIEW_HEIGHT: number = 500;
​
/** @name 列表项高度 */
​
const ITEM_HEIGHT: number = 50;
​
/** @name 预加载数量 */
​
const PRE_LOAD_COUNT: number = SCROLL_VIEW_HEIGHT / ITEM_HEIGHT;

接着,创建一个useRef用来存储元素,然后获取视窗高度和偏移属性。

/** 容器Ref */const containerRef = useRef<HTMLDivElement | null>(null);

然后,创建数据源,并且生成3000条随机数据做显示处理。

const [sourceData, setSourceData] = useState<number[]>([]);
​
/**
 * 创建列表显示数据
 */
const createListData = () => {
  const initnalList: number[] = Array.from(Array(4000).keys());
  setSourceData(initnalList);
};
​
useEffect(() => {
  createListData();
}, []);
​

最后,为相对应的容器绑定高度。在最外层div标签设置高度为SCROLL_VIEW_HEIGHT,对列表div的高度则设置为sourceData.length * ITEM_HEIGHT

获取列表整体高度
/**
 * scrollView整体高度
 */
 const scrollViewHeight = useMemo(() => {
  return sourceData.length * ITEM_HEIGHT;
}, [sourceData]);
​
绑定页面视图
<div
  ref={containerRef}
  style={{
    height: SCROLL_VIEW_HEIGHT,
    overflow: "auto",
  }}
  onScroll={onContainerScroll}
>
  <div
    style={{
      width: "100%",
      height: scrollViewHeight - scrollViewOffset,
      marginTop: scrollViewOffset,
    }}
  >
    {sourceData.map((e) => (
      <div
        style={{
          height: ITEM_HEIGHT,
        }}
        className="showElement"
        key={e}
      >
        Current Position: {e}
      </div>
    ))}
  </div>
</div>;

当数据初始化后,我们的列表页面就初步完成了,来看下效果吧。

image.png

内容截取

对于虚拟列表来说,并不需要全量将数据渲染在页面上。那么,在这里我们就要开始做数据截取的工作了。

首先,如下图,我们通过showRange来控制页面显示元素的数量。通过Array.slice的函数方法对sourceData进行数据截取, 返回值就是我们在页面上去显示的列表数据了。我将上面代码中直接遍历souceData换成我们的新数据列表。如下:

{currentViewList.map((e) => (
  <div
    style={{
      height: ITEM_HEIGHT
    }}
    className="showElement"
    key={e.data}
  >
    Current Position: {e.data}
  </div>
))}

上面使用到的currentViewList是一个useMemo的返回值,它会随着showRangesourceData的更新发生变化。

/**
 * 当前scrollView展示列表
 */
 const currentViewList = useMemo(() => {
  return sourceData.slice(showRange.start, showRange.end).map((el, index) => ({
    data: el,
    index,
  }));
}, [showRange, sourceData]);

image.png

滚动计算

至此,已经完成了一个基本的虚拟列表雏形,下一步我们就需要监听视窗滚动事件来计算showRange中的startend的偏移量,同时调整对应的滚动条进度来实现一个真正的列表效果。

首先,我先为滚动视窗(scrollContainer)绑定onScroll事件,也就是下面的onContainerScroll函数方法。

/**
 * onScroll事件回调
 * @param event { UIEvent<HTMLDivElement> } scrollview滚动参数
 */
 const onContainerScroll = (event: UIEvent<HTMLDivElement>) => {
  event.preventDefault();
  calculateRange();
};

在事件主要做的事情就计算当前showRange中的startend所处位置,同时更新页面视图数据。下面,我们来看看它是怎么处理的吧!

首先,通过containerRef.current.scrollTop可以知道元素滚动条内的顶部隐藏列表的高度,然后使用Math.floor方法向下取整后,来获取当前偏移的元素数量,在减去一开始的上下文预加载数量PRE_LOAD_COUNT,就可以得出截取内容开始的位置。

其次,通过containerRef.current.clientHeight可以获取滚动视窗的高度,那么通过containerRef.current.clientHeight / ITEM_HEIGHT这个公式就可以得出当前容器窗口可以容纳几个列表项。

当我通过当前滚动条位置下之前滚动的元素个数且已经计算出截取窗口的起始位置后,就可以通过启动位置 + 容器显示个数 + 预加载个数这个公式计算出了当前截取窗口的结束位置。使用setShowPageRange方法更新新的位置下标后,当我上下滑动窗口,显示的数据会根据showRange切割成为不同的数据渲染在页面上。

/**
 * 计算元素范围
 */
 const calculateRange = () => {
  const element = containerRef.current;
  if (element) {
    const offset: number = Math.floor(element.scrollTop / ITEM_HEIGHT) + 1;
    console.log(offset, "offset");
    const viewItemSize: number = Math.ceil(element.clientHeight / ITEM_HEIGHT);
    const startSize: number = offset - PRE_LOAD_COUNT;
    const endSize: number = viewItemSize + offset + PRE_LOAD_COUNT;
    setShowPageRange({
      start: startSize < 0 ? 0 : startSize,
      end: endSize > sourceData.length ? sourceData.length : endSize,
    });
  }
};

image.png

滚动条偏移

上面,我们提到会根据containerRef.current.scrollTop计算当前滚动过的高度。那么问题来了,页面上其实并没有真实的元素,又该如何去撑开这个高度呢?

目前而言,比较流行的解决方案分为MarinTopTranForm做距离顶部的偏移来实现高度的撑开。

  • margin是属于布局属性,该属性的变化会导致页面的重排
  • transform是合成属性,浏览器会为元素创建一个独立的复合层,当元素内容没有发生变化,该层不会被重绘,通过重新复合来创建动画帧。

两种方案并没有太大的区别,都可以用来实现距离顶部位置的偏移,达到撑开列表实际高度的作用。

下面,我就以MarinTop的方法来处理这个问题,来完善当前的虚拟列表。

首先,我们需要计算出列表页面距离顶部的MarginTop的距离,通过公式:当前虚拟列表的起始位置 * 列表项高度,我们可以计算出当前的scrollTop距离。

通过useMemo将逻辑做一个缓存处理,依赖项为showRange.start, 当showRange.start发生变化时会更新marginTop的高度计算。

/**
 * scrollView 偏移量
 */
 const scrollViewOffset = useMemo(() => {
  console.log(showRange.start, "showRange.start");
  return showRange.start * ITEM_HEIGHT;
}, [showRange.start]);

在页面上为列表窗口绑定marginTop: scrollViewOffset属性,并且在总高度中减去scrollViewOffset来维持平衡,防止多出距离的白底。

如下代码
<div
    style={{
        width: "100%",
        height: scrollViewHeight - scrollViewOffset,
        marginTop: scrollViewOffset
    }}
>

至此,我们已经完成了一个基本的虚拟列表,下面我们来一起看看实际的效果吧。

Kapture 2021-08-08 at 17.51.29.gif

结合分页加载

当我们有了一个虚拟列表后,就可以尝试结合分页加载来实现一个懒加载的长虚拟列表了。

如果做过分页滚动加载的小伙伴可能立马就想到实现思路了,不了解的同学也不要着急,下面我就带大家一起来实现一个带分页加载的虚拟列表,相信你看完之后会对这类问题有一个更加深入的理解。

判断是否到底部

想要实现列表的分页加载,我们需要绑定onScroll事件来判断当前滚动视窗是否滚动到了底部,当滚动到底部后需要为sourceData进行数据的添加。同时将挪动指针,将数据指向下一个起始点。

具体实现代码如下,reachScrollBottom函数的返回值是当前滚动窗口是否已经到达了底部。因此,我们通过函数的返回值进行条件判断。到达底部后,我们模拟一批数据后通过setSourceData设置源数据。结束之后在执行calculateRange重新设置内容截取的区间。

/**
 * onScroll事件回调
 * @param event { UIEvent<HTMLDivElement> } scrollview滚动参数
 */
 const onContainerScroll = (event: UIEvent<HTMLDivElement>) => {
  event.preventDefault();
  if (reachScrollBottom()) {
    // 模拟数据添加,实际上是 await 异步请求做为数据的添加
    let endIndex = showRange.end;
    let pushData: number[] = [];
    for (let index = 0; index < 20; index++) {
      pushData.push(endIndex++);
    }
    setSourceData((arr) => {
      return [...arr, ...pushData];
    });
  }
  calculateRange();
};

那么,calculatScrollTop是如何判断当前是否已经触底呢?

image.png

分析上图,我通过containerRef可以拿到滚动窗口的高度scrollHeight或者直接使用soureData.length * ITEM_HEIGHT充当滚动窗口的高度两者作用是一样的。

同时,我也可以拿到scrollTop滚动位置距离顶部的高度和clientHeight当前视窗高度。通过三者的关系,可以得出条件公式:scrollTop + clientHeight >= scrollHeight,满足这个条件就说明当前窗口已经到达底部。我们将其写成reachScrollBottom方法,如下:

/**
 * 计算当前是否已经到底底部
 * @returns 是否到达底部
 */
 const reachScrollBottom = (): boolean => {
  //滚动条距离顶部
  const contentScrollTop = containerRef.current?.scrollTop || 0; 
  //可视区域
  const clientHeight = containerRef.current?.clientHeight || 0; 
  //滚动条内容的总高度
  const scrollHeight = containerRef.current?.scrollHeight || 0;
  if (contentScrollTop + clientHeight >= scrollHeight) {
    return true;
  }
  return false;
};

本篇文章中,我讲了针对商城项目中出现长列表的部分场景,同时针对这些场景列举了不同的解决方案及其优缺点。在选择分页 + 虚拟列表的组合方式来解决问题的过程中,我一步一步带大家实现了一个简单的分页虚拟列表,帮助大家了解其内部的原理。

当然,这个方案还有很多需要完善的地方,我也在这里说说它需要优化的地方。

  • 滚动事件可以添加节流事件避免造成性能浪费。
  • 列表项高度不固定需要给定一个默认高度后设置新的高度在重新刷新容易截取的开始和结束位置。
  • 滑动过快出现白屏问题可以尝试动态加载loading显示过渡,优化一些细节体验。
  • 列表项中存在阴影元素需要考虑缓存处理,不然滚动时必然会引起重新加载。

4.图片懒加载分析

www.cnblogs.com/tugenhua070…

懒加载与预加载的基本概念。

懒加载也叫延迟加载: 前一篇文章有介绍:JS图片延迟加载 延迟加载图片或符合某些条件时才加载某些图片。

预加载: 提前加载图片,当用户需要查看时可直接从本地缓存中渲染。

两种技术的本质: 两者的行为是相反的,一个是提前加载,一个是迟缓甚至不加载。懒加载对服务器前端有一定的缓解压力作用,预加载则会增加服务器前端压力。

懒加载的意义及实现方式有:

意义: 懒加载的主要目的是作为服务器前端的优化,减少请求数或延迟请求数。

实现方式:

1.第一种是纯粹的延迟加载,使用setTimeOut或setInterval进行加载延迟.

2.第二种是条件加载,符合某些条件,或触发了某些事件才开始异步下载。

3.第三种是可视区加载,即仅加载用户可以看到的区域,这个主要由监控滚动条来实现,一般会在距用户看到某图片前一定距离遍开始加载,这样能保证用户拉下时正好能看到图片。

预加载的意义及实现方式有:

预加载可以说是牺牲服务器前端性能,换取更好的用户体验,这样可以使用户的操作得到最快的反映。实现预载的方法非常多,可以用CSS(background)、JS(Image)、HTML()都可以。常用的是new Image();,设置其src来实现预载,再使用onload方法回调预载完成事件。只要浏览器把图片下载到本地,同样的src就会使用缓存,这是最基本也是最实用的预载方法。当Image下载完图片头后,会得到宽和高,因此可以在预载前得到图片的大小(方法是用记时器轮循宽高变化)。

怎么样才能实现预加载?

我们可以通过google一搜索:可以看到很多人用这种方式进行预加载:代码如下:

function loadImage(url,callback) {
    var img = new Image();
    
    img.src = url;
    img.onload = function(){
        img.onload = null;
        callback.call(img);
    }
}

在google或者火狐下测试 都是正常的 不管我怎么刷新都是正常的,但是在IE6下不是这样的 我点击一下 是正常 再次点击或者重新刷新都不正常。下面的jsfiddle地址:有兴趣的同学可以试试 点击按钮后 弹出正常结果 再次点击在IE6下就不执行onload里面的方法了,接着重新刷新也不行。

为什么其他浏览器正常的:其实原因很简单,就是浏览器缓存了,除了IE6以外(即说opera也会,但是我特意用opera试了下,没有,可能版本的问题吧,或许现在已经修复了。),其他浏览器重新点击会再次执行onload方法,但是IE6是直接从浏览器取的。

那现在怎么办?最好的情况是Image可以有一个状态值表明它是否已经载入成功了。从缓存加载的时候,因为不需要等待,这个状态值就直接是表明已经下载了,而从http请求加载时,因为需要等待下载,这个值显示为未完成。这样的话,就可以搞定了。经过google搜索下即介绍:发现有一个为各个浏览器所兼容的Image的属性——complete。所以,在图片onload事件之前先对这个值做一下判断即可。最后,代码变成如下的样子:

function loadImage(url,callback) {
    var img = new Image();
    
    img.src = url;

    if(img.complete) {  // 如果图片已经存在于浏览器缓存,直接调用回调函数
        
        callback.call(img);
        return; // 直接返回,不用再处理onload事件
    }

    img.onload = function(){
        img.onload = null;
        callback.call(img);
    }
}

也就是说如果图片已经在浏览器缓存里面 那么支持直接从浏览器缓存取得直接执行img.complete里面的函数 接着返回.

但是我们可以看到上面的代码:必须等图片加载完成后,可以执行回调函数,也可以说等图片加载后,我们可以获取图片的宽度和高度。那么如果我们想提前获取图片的尺寸那怎么办?上网经验告诉我:浏览器在加载图片的时候你会看到图片会先占用一块地然后才慢慢加载完毕,并且不需要预设width与height属性,因为浏览器能够获取图片的头部数据。基于此,只需要使用javascript定时侦测图片的尺寸状态便可得知图片尺寸就绪的状态。代码如下:(但是有个前提是 这个方式不是我想的,也不是我写的代码,是网上朋友总结的代码 我只是知道有这么一个原理)

var imgReady = (function(){
    var list = [],
        intervalId = null;

    // 用来执行队列
    var queue = function(){

        for(var i = 0; i < list.length; i++){
            list[i].end ? list.splice(i--,1) : list[i]();
        }
        !list.length && stop();
    };
    
    // 停止所有定时器队列
    var stop = function(){
        clearInterval(intervalId);
        intervalId = null;
    }
    return function(url, ready, error) {
        var onready = {}, 
            width, 
            height, 
            newWidth, 
            newHeight,
            img = new Image();
        img.src = url;

        // 如果图片被缓存,则直接返回缓存数据
        if(img.complete) {
            ready.call(img);
            return;
        }
        width = img.width;
        height = img.height;

        // 加载错误后的事件 
        img.onerror = function () {
            error && error.call(img);
            onready.end = true;
            img = img.onload = img.onerror = null;
        };

        // 图片尺寸就绪
        var onready = function() {
            newWidth = img.width;
            newHeight = img.height;
            if (newWidth !== width || newHeight !== height ||
                // 如果图片已经在其他地方加载可使用面积检测
                newWidth * newHeight > 1024
            ) {
                ready.call(img);
                onready.end = true;
            };
        };
        onready();
        // 完全加载完毕的事件
        img.onload = function () {
            // onload在定时器时间差范围内可能比onready快
            // 这里进行检查并保证onready优先执行
            !onready.end && onready();
            // IE gif动画会循环执行onload,置空onload即可
            img = img.onload = img.onerror = null;
        };
        
        
        // 加入队列中定期执行
        if (!onready.end) {
            list.push(onready);
            // 无论何时只允许出现一个定时器,减少浏览器性能损耗
            if (intervalId === null) {
                intervalId = setInterval(queue, 40);
            };
        };
    }
})();

用方式如下:

imgReady('img01.taobaocdn.com/imgextra/i1…',function(){   alert('width:' + this.width + 'height:' + this.height); });

具体实现原理

有时候一个网页会包含很多的图片,例如淘宝京东这些购物网站,商品图片多只之又多,页面图片多,加载的图片就多。服务器压力就会很大。不仅影响渲染速度还会浪费带宽。比如一个1M大小的图片,并发情况下,达到1000并发,即同时有1000个人访问,就会产生1个G的带宽。

为了解决以上问题,提高用户体验,就出现了懒加载方式来减轻服务器的压力,优先加载可视区域的内容,其他部分等进入了可视区域再加载,从而提高性能。

vue项目中的打包,是把html、css、js进行打包,还有图片压缩。但是打包时把css和js都分成了几部分,这样就不至于一个css和就是文件非常大。也是优化性能的一种方式。 效果动图如下:

进入正题------懒加载

1.懒加载原理 一张图片就是一个标签,浏览器是否发起请求图片是根据的src属性,所以实现懒加载的关键就是,在图片没有进入可视区域时,先不给的src赋值,这样浏览器就不会发送请求了,等到图片进入可视区域再给src赋值。

2.懒加载思路及实现 实现懒加载有四个步骤,如下: 1.加载loading图片 2.判断哪些图片要加载【重点】 3.隐形加载图片 4.替换真图片

1.加载loading图片是在html部分就实现的,代码如下:

2.如何判断图片进入可视区域是关键。 引用网友的一张图,可以很清楚的看出可视区域。

如上图所示,让在浏览器可视区域的图片显示,可视区域外的不显示,所以当图片距离顶部的距离top-height等于可视区域h和滚动区域高度s之和时说明图片马上就要进入可视区了,就是说当top-height<=s+h时,图片在可视区。 这里介绍下几个API函数: 页可见区域宽: document.body.clientWidth; 网页可见区域高: document.body.clientHeight; 网页可见区域宽: document.body.offsetWidth (包括边线的宽); 网页可见区域高: document.body.offsetHeight (包括边线的宽); 网页正文全文宽: document.body.scrollWidth; 网页正文全文高: document.body.scrollHeight; 网页被卷去的高: document.body.scrollTop; 网页被卷去的左: document.body.scrollLeft; 网页正文部分上: window.screenTop; 网页正文部分左: window.screenLeft; 屏幕分辨率的高: window.screen.height; 屏幕分辨率的宽: window.screen.width; 屏幕可用工作区高度: window.screen.availHeight;

HTMLElement.offsetTop 为只读属性,它返回当前元素相对于其 offsetParent 元素的顶部的距离。 window.innerHeight:浏览器窗口的视口(viewport)高度(以像素为单位);如果有水平滚动条,也包括滚动条高度。

具体实现的js代码为:

// onload是等所有的资源文件加载完毕以后再绑定事件 window.onload = function(){ // 获取图片列表,即img标签列表 var imgs = document.querySelectorAll('img');

// 获取到浏览器顶部的距离
function getTop(e){
	return e.offsetTop;
}

// 懒加载实现
function lazyload(imgs){
	// 可视区域高度
	var h = window.innerHeight;
	//滚动区域高度
	var s = document.documentElement.scrollTop || document.body.scrollTop;
	for(var i=0;i<imgs.length;i++){
		//图片距离顶部的距离大于可视区域和滚动区域之和时懒加载
		if ((h+s)>getTop(imgs[i])) {
			// 真实情况是页面开始有2秒空白,所以使用setTimeout定时2s
			(function(i){
				setTimeout(function(){
					// 不加立即执行函数i会等于9
					// 隐形加载图片或其他资源,
					//创建一个临时图片,这个图片在内存中不会到页面上去。实现隐形加载
					var temp = new Image();
					temp.src = imgs[i].getAttribute('data-src');//只会请求一次
					// onload判断图片加载完毕,真是图片加载完毕,再赋值给dom节点
					temp.onload = function(){
						// 获取自定义属性data-src,用真图片替换假图片
						imgs[i].src = imgs[i].getAttribute('data-src')
					}
				},2000)
			})(i)
		}
	}
}
lazyload(imgs);

// 滚屏函数
window.onscroll =function(){
	lazyload(imgs);
}

效果如下:

随着鼠标向下滚动,其余图片也逐渐显示并发起请求。

效果动图如下:

5.项目重构

什么是重构

我们开发惯指的 重构 ,一般都是指技术重构。简单点说就是基于项目进行代码层面的重构。推倒了重新来,老房子扒掉重新造,肯定是有钱了想让自己更舒适,程序代码推倒了重新写,还不是因为代码质量经过长年累月需求迭代,祖传代码越来越难维护,更别说在这个基础上去老树开花,开发一些新功能。(代码太烂,遗留的坑太多,就是程序的拓展性和维护性不好呗画外音,前浪们留下的一堆堆精华 💩 ,需要后狼们一铲一铲地拍在 上……)

那么问题来了,你的项目到底需不需要重构呢

考虑到项目重构带来的人力、时间、项目风险等因素,在商业项目中,推倒重来是一个风险高,收益低,吃力不太讨好的事情。而且,推翻之前的项目重做,也不定会写出比以前更好的代码。那为什么还要重构呢,或许我们从业务和团队角度分析能得到一些答案。

业务角度分析

  1. 业务转型了,基于原有业务做得系统自然成了前朝遗老,不招人稀罕了,别说重构,废弃都是有可能的。
  2. 业务体量变化,原先的技术架构可能对于百人内的团队,性能上瓶颈不明显,但是随着业务体量的上涨,对于产品性能、扩展性、稳定性的要求越来越高,会推动当前产品迭代及重构的需求

团队角度分析

  1. 当前技术方案的问题:单签方案是否影响团队开发效率,项目技术方案是否比较陈旧,难以维护,是否存在家属架构及依赖包过于老旧的问题。如果你的项目依赖文件人家官方都已经不维护了,而且官方文档也给出了相关替换方案,那你的项目确实该进行升级、迭代,甚至是换一套新的技术栈进行重构了。
  2. 当前项目的代码本身的问题:代码是否基于团队规范标准开发,代码是否有较好的拓展性、健壮性和可维护性。项目代码经过长期迭代,多人轮换,没有规范标准的情况下,代码会变得越来越难维护,一个文件动辄千八百行代码,不用驼峰,不用清晰语义命名,不写代码注释,分分钟逼死强逼症,这样的代码,加个新功能,都要反反复复的翻以前的代码,即使改好了,还有可能因为,之前项目代码不够健壮,报出来其他奇奇怪怪的问题。

那前端开发在项目重构中能干点啥呢

  1. 无用的三方库看着不碍眼吗,删掉啊
  2. 一些三方库只用了一两次,自写功能成本也不是很高,留着干啥
  3. 删除无用变量|无用import 文件
  4. 删除用不到的逻辑,精简、抽分通用逻辑
  5. 拆分大文件,动辄千八百行的代码文件,不抽分,后期只会越来越多,后期维护成本越来越高,重构代价也越来越大
  6. 减少全局样式,采用 css modules 做样式隔离,避免绞尽脑计想命名,也避免跟某个组件库样式冲突
  7. 代码结构重构,优化项目工程目录结构,项目迭代下来,会有很多重复的文件目录结构,应该从项目整体角度考虑,合理划分目录结构
  8. 代码命名、模块抽分、合理注释总得加一下吧
  9. 一些无用的,当时测试用的 console,debugger 看到就删掉呗
  10. 做一些必要的依赖升级,项目依赖包一直在升级,为了项目长期稳定的使用依赖包的一些能力,必要的依赖包升级还是有必要的

重构时应该注意哪些问题呢

  1. 首先,很认真的问下自己,问下团队相关成员,这个项目是真的需要重构吗,软件迭代是必需的,但是重构真的不是必要的,必要打碎了,重新来过,不一定比之前做的更好
  2. 重构时,你要对重构的项目有必要的理解,知道当初这个功能实现的初衷,才能保证重构后的版本,不会有其他不好的影响,建议重构过程中,多看之前的逻辑实现,多问当时参与的人,相关的产品经理、开发,甚至是测试,了解到被注释掉的代码,是否是没用了,真没用了,再扔掉,否则,一刀切,很可能,后期你还得补回来
  3. 重构的目的要清楚,你是重构一个组件,一个模块,还是整个系统,整个系统推倒重来,对于任何公司来说都是一个慎重的事情,比较好的做法是,渐进式的重构,把系统切成相互独立的小块,一点一点迭代,可以作为日常迭代,也可以做成专项迭代,看业务需求
  4. 架构选型,不一定是什么新,什么流行用什么,得考虑团队或者个人的学习成本,可能这个新技术确实很好,但是现有团队业务开发任务很重,没有必要一步登天,折磨自己,折磨别人,一句话适合自己的才是最好的
  5. 明确重构的目的是为了,让项目不像老代码那样臃肿,难以维护,那么定一些标准化的参考规则是很有必要的,最起码保证相当长的时间内,看着像一个正经的项目

我个人在重构过程中的一些习惯(仅供参考)

  1. 首先,我会梳理现有项目代码,对照项目页面,给老项目加一点注释标记
  2. 创建项目结构 + 功能脑图,项目干了点啥,需要哪些功能一目了然,后期开发,参照起来,安排排期、预估开发进度,个人感觉还挺有用的
  3. 标记问题,老项目缺少注释,文件结构混乱是常有的事儿,遇到不理解的,多思多问是个好习惯,提前把风险点记录下来,可以用来评估,这个项目重构带来的结果是不是正向的
  4. 参照通用规范,梳理开发标准,像 css、js 的变量命名,模块抽分标准这样还是要有个可参考的开发标准的
  5. 基础技术栈统一,一个项目js、ts 混着用,可能是不好的,鉴于现在前端的发展趋势,大方向上使用 ts 会是未来几年的大趋势,也避免了 js 弱类型带来的一些负面影响,样式管理的话,我这边采用的是 less + css module 来做,这样命名相对清晰,也不会造成样式文件相互影响

重构方案

前端重构规划

6.性能问题排查

前端性能优化利器

设计方法

实现扫码登录

相关文章

web即时通讯

轮询、长轮询、长连接、websocket

前端埋点和性能监控

埋点分析,是网站分析的一种常用的数据采集方法

性能监控

在小项目时,由于用户数量不多,大家觉得过得去就行,而当用户数量激增以后,性能监控,就显得非常重要,因为,这样你能就能知道潜在的一些问题和bug,并且能快速迭代,获得更好的用户体验!一般情况下,我们在性能监控时需要注意那么几点:

  • 1、白屏时长
  • 2、重要页面的http请求时间
  • 3、重要页面的渲染时间
  • 4、首屏加载时长

有人就会问了,这个白屏时长和首屏加载时长不是一回事吗?这里的白屏时长其实指的时,页面从请求到达到渲染条件,出现ui骨架的时间(这里测试的是请求域名到dns解析完毕,返回页面骨架的时间)而首屏加载时长是页面所有动态内容加载完成的时间,其中包括ajax数据后渲染到页面的时间

数据监控

所谓数据监控就是能拿到用户的行为,我们也需要注意那么几点:

  • 1、PV访问来量(Page View)
  • 2、UV访问数(Unique Visitor)
  • 3、记录操作系统和浏览器
  • 4、记录用户在页面的停留时间
  • 5、进入当前页面的来源网页(也就是从哪进来的转化)

如何埋点

手动埋点也叫代码埋点,他的本质其实就是用js代码拿到一些基本信息,然后在一些特定的位置返回给服务端,比如:

img

如上图我们可以拿到这些内容,再比如:

img

我还可以拿到这些,有人就有疑问了,这些我咋拿到呢?

Performance

通过Performance 我们便能拿到DNS 解析时间、TCP 建立连接时间、首页白屏时间、DOM 渲染完成时间、页面 load 时间等,等等 废话少说上代码:

//拿到Performance并且初始化一些参数
let timing = performance.timing,
    start = timing.navigationStart,
    dnsTime = 0,
    tcpTime = 0,
    firstPaintTime = 0,
    domRenderTime = 0,
    loadTime = 0;
//根据提供的api和属性,拿到对应的时间
dnsTime = timing.domainLookupEnd - timing.domainLookupStart;
tcpTime = timing.connectEnd - timing.connectStart;
firstPaintTime = timing.responseStart - start;
domRenderTime = timing.domContentLoadedEventEnd - start;
loadTime = timing.loadEventEnd - start;

console.log('DNS解析时间:', dnsTime, 
            '\nTCP建立时间:', tcpTime, 
            '\n首屏时间:', firstPaintTime,
            '\ndom渲染完成时间:', domRenderTime, 
            '\n页面onload时间:', loadTime);
复制代码

img

拿到数据以后我们可以在提交,或者通过图片的方式去提交埋点内容

  // 页面加载时发送埋点请求
$(document).ready(function(){
 // ... 这里存在一些业务逻辑
 sendRequest(params);
});
// 按钮点击时发送埋点请求
$('button').click(function(){
   //  这里存在一些业务逻辑
   sendRequest(params);
});
  // 通过伪装成 Image 对象,传递给后端,防止跨域
    let img = new Image(1, 1);
    let src = `http://aaaaa/api/test.jpg?args=${encodeURIComponent(args)}`;
    img.src = src;
//css实现的埋点
    .link:active::after{
    content: url("http://www.example.com?action=yourdata");
}
<a class="link">点击我,会发埋点数据</a>
//data自定义属性,rangjs去拿到属性绑定事件,实现埋点
//<button data-mydata="{key:'uber_comt_share_ck', act: 'click',msg:{}}">打车</button>

这种埋点方式虽然能精准的监控到用户的行为,和网页性能等数据,但是你会发现,非常繁琐,需要大量的工作量,当然这部分工作也有人帮我们做了,比如像友盟、百度统计等给我们其实提供了服务。我们可以按照他们的流程使用手动埋点

无埋点

无埋点并不是没有任何埋点,所谓无只是不需要工程师在业务代码里面插入侵入式的代码。只需要简单的加载了一段定义好的SDK代码,技术门槛更低,使用与部署也简单,避免了需求变更,埋点错误导致的重新埋点。这也是大多网站的选择,因为实在太简单了 我们先来看看百度埋点长什么样子:

 <script>
      var _hmt = _hmt || []
      ;(function() {
        var hm = document.createElement('script')
        hm.src =
          'https://hm.baidu.com/hm.js?<%= htmlWebpackPlugin.options.baiduCode %>'
        var s = document.getElementsByTagName('script')[0]
        s.parentNode.insertBefore(hm, s)
      })()
    </script>
复制代码

上图一段代码插入我们的html中

img

我们便能清晰的看到统计数据,省时省力,就是不省钱!但是缺点就是由于是自动完成,无法针对特定场景拿到数据,由后端来过滤和计算出有用的数据。导致服务器压力山大,不过,既然花了钱了,咱也就不管了!

要让你设计一个前端统计 SDK ,你会如何设计?

前端统计的范围

  • 访问量 PV
  • 自定义事件(如统计一个按钮被点击了多少次)
  • 性能
  • 错误

统计数据的流程 (只做前端 SDK ,但是要了解全局)

  • 前端发送统计数据给服务端
  • 服务端接受,并处理统计数据
  • 查看统计结果

如何设计一个h5抽奖页面

  • 获取用户信息(同时判断是否登录)

  • 如果登录,判断该用户是否已经抽奖,以判断他是否还能继续抽奖

  • 抽奖接口

    • 可能还需要调用登录接口
    • 当然也可以直接输入手机号抽奖,需明确需求
  • 埋点统计

    • pv
    • 自定义事件
  • 微信分享

组件设计的思路,怎么封装组件。

一起学习主流方案

毕竟做什么事情都要有基础,有多个学习的对象,在我们什么都不会的情况下,我们需要学习主流的组件库架构方案,我们学习了vant, vuetify, element, element-plus, material-ui,muse-ui等等许多组件库的架构思想, 一边摸索一边思考适合自己的实现。

Monorepo 架构

我们采用了拆包的架构, 主要是通过yarn workspacelerna实现,好处在于我们可以把通用的依赖都做成一个包进行单独发布,在构建组件库的过程中也可以同时产出一些实用的工具,也为后期项目的扩展打下了基础。同时lerna有着完善的发包机制,让我们不需要太关心包和包之间的依赖关系。组件库则设计成其中的一个子包,所以Varlet在未来可能不会仅仅是一个组件库,随着包的增多可能会变成一个解决方案,实际上我们也正在朝这个方向探索。

Design System

首先,作者不建议在没有设计系统的情况下进行组件库的开发,因为自己拍脑门想出来的设计总是会那么的不合理。如果企业有自己的能力设计一个风格或是设计系统那是最好的选择。 如果像是作者这样的倒霉蛋,也最好选择开源并且成熟的设计系统。比如我们选择了Material Design,作者的一个朋友也正在做自己的组件库,他选择了Vercel Design,这里是他的 Github仓库 有兴趣的可以去看看,捧个场,我们应该尊重每个有分享精神的人。

uisdc-yk-20181104-69.jpeg

相关工具

构建一个组件库,需要的工具又广又杂,我们考虑到一个成熟的组件库至少应该满足以下最基本的开发要求

  • 开发环境,你得起个服务去调试代码吧
  • 支持按需引入,应该没有人愿意全量导入组件库把
  • 组件库编译,生成umdesm模块的组件代码
  • 构建开发文档,至少得有个中文文档说明一下组件怎么用吧
  • 单元测试,你写的代码得信的过吧
  • 桌面端和移动端的组件预览,你得让使用者看到组件具体长什么样子吧
  • 代码格式化和规范检测工具,毕竟是团队作案,没有规矩不成方圆
  • 自动化的文档部署和测试流程,总不能每次发布版本都手动去部署文档和测试吧

所以我们决定自己实现上面所有的功能,并且单独抽了一个子包,叫作Varlet-Cli,这个包如今也开源了出来,很大程度上降低了开发组件库的门槛。使用手册在 这里 ,具体实现可以去我们的 Github仓库 去看源码

开发环境

我们的开发环境采用了webpack5webpack-dev-server构建了一个官方文档站点,基于我们自定义的插件进行了src目录的扫描,提取出了有用的信息并构建了基础的路由和文档配置。关于文档编写我们是通过varlet-markdown-loadermd文件编译成了vue template string渲染在了每一个路由模块中,使得文档编写更加容易。

为什么不是Vite

说句实在话,在我们去年十月份准备开始动手的时候,Vite并不稳定,现在也没有一定要换Vite作为开发环境的理由,或许以后有更换的可能,但是我们目前还是会将精力聚焦到更重要的事情上,对于个人开发者来说,合理安排时间做更有效的产出是最重要的。但是对于一个新的项目,我认为Vite应该是第一选择,因为它真的非常非常优秀

组件库编译器

在有了开发环境之后,我们还需要把我们的组件代码导出成umdesm模块来提供给用户使用,这里我们讨论之后没有使用rollup,而是选择自己实现组件库的编译器。编译组件其实核心就是扫描整个目录,扫到什么格式的文件就用对应的编译器去过一遍他,这个没什么难度,自己实现可以在编译过程中添加很多的优化,并且是完全自由可控的,可以生成我们希望生成的模块结构,也方便我们实现按需引入,事实也证明了,这很值得。

组件原型设计与重构

当我们开始面向具体的场景进行组件开发的时候,我们会各自阐述自己对于这个组件的理解,并且由负责这个组件的人牵头去做原型开发,也就是草稿,因为talk is cheap,所以需要定一个大概的雏型并做具体实现。当原型开发结束之后,我们再次对原型进行评审,进行深入的讨论,最后负责该组件的人会对组件进行重写,确定api,补全文档,完成单测,最后发布。不要畏惧重写和推翻,也不要奢求一步到位,是我们慢慢总结出来的一些经验之谈。

组件单元测试编写

为了组件的稳定性以及减少维护压力,每个功能都需要进行完善的单元测试,我们使用jest + @vue/test-utils进行测试,这两个包也是vue官方推荐的,虽然可能需要自行封装一些手势相关的工具函数,但是总体来看还是比较轻量易用的。然后需要使用jest生成测试报告,并托管到codecovcodecov是一个开源的测试结果展示平台, 可以将测试结果可视化。

截屏2021-09-22 上午12.24.49.png

截屏2021-09-22 上午12.39.00.png

组件发布

我们遵循semver 语义化版本规范, 也就是1(主版本号).0(次版本号).0(修订版本号)这样的模式。有破坏性的更新动第一位,有新功能动第二位,改改bug动第三位。大部分的开源项目都遵循这个规范,所以我们尽量不要随心所欲的动我们的版本号。当你完成了一个版本,但是对版本的更新内容心里没底的时候,记得先发alpha版本,进行生产上的反复测试。对于已经有很大用户量的项目,应该每次更新都有着alpha, beta, rc乃至更多的先行版本,来给正式版本提供可靠性保障。

文档部署

文档部署在哪里是一个问题,对于大部分的人来说,可能没有精力去维护一个静态服务器。这里我推荐gitee pagesgithub pages,它们可以帮我们托管静态资源,并且是免费的。

截屏2021-09-22 上午12.41.29.png

Git Actions 自动部署和测试

当项目的贡献者越来越多时,一些贡献者不可避免会犯一些流程错误。比如提交代码时忘记跑单元测试,没有尝试对项目进行生产模式的构建等等,为了避免错误,我们需要在提交代码到git远程仓库时去做一些流程性的任务,也就是我们常说的ci/cd或者流水线。又比如说每次发布新版本,不可避免的需要对文档进行重新部署,每次都手动部署也太麻烦了,这种流程性的任务也可以解决我们的问题。 Git Actions可以很好的解决我们的问题,我们可以让它帮我们执行单元测试和代码校验,帮我们做githubgitee的同步,帮我们做文档的部署,解放我们的双手,减少错误的发生。

截屏2021-09-22 上午12.33.35.png

PR,ISSUE规范

做一个开源项目一定会收到许多prissue,但是很多人并不清楚仓库所有者最需要的信息是什么,为了更快的定位bug和解决问题,可以在github仓库提供prissue的模板来解决这一问题。也可以提供贡献指南或者开发手册,让有兴趣的人更快的可以参与进来。

截屏2021-09-22 上午12.46.50.png

这件事做的还不错,会让大家十分开心。感谢以下小伙伴们的贡献,以后我们继续加油,继续快乐

有没有看elementUI源码,为什么那么设计

首先 Element 有几个版本,我看的是基于 Vue 的版本,所以每个组件到底就是一个 vue 文件,就和我们平时工作写的代码一样,写好一个 vue 组件,然后在需要的页面引入即可。不过更重要的是要知道如何写好这个组件(健壮吗,可扩展吗,易维护吗等)。一个 vue 组件一般可分为三部分,templatescriptstyle。在这里我们就不考虑 style 了,直接在页面引用 Element 的样式就好,因为这不是我们主要关心的,我们只要知道 Element 的样式一般是这样(el-组件名--状态,比如 el-button--primary)命名的就行。所以我们组件里是没有写 style 部分的,这样做能帮我们省下好多时间和精力。

// 直接在页面中引入 Element 的样式
<link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">

先看 template 部分

那么接下来我们就先看看 template 的部分怎么写。其实这部分是很简单的(对于这个组件来说😁),我们可以先打开 Element 文档看一下 button 的外观样式,再来写这部分,它大概长下面这样:

img ok,假设你已经看过 button 组件的大部分外观,接下来我们就可以在脑海中先想一下(抽离并化简一下 html 结构的公共部分),大概就是一个 div(button 标签)里面包了一个 i 图标和 span 文本这样的结构,嗯好像是这样,那就试着写一下吧!(提示:Element 组件一般最外层的样式都是用 el-组件名 包起来的)

<template>
  <button class="el-button">
    <i></i>
    <span></span>
  </button>
</template>

看上面的结构像那么回事,也简单明了。不过,然后呢😯。。。 然后就是我们的 script 部分啦,这个才是组件的灵魂所在,重中之重,也是需要我们去啃的部分。好在这个组件简单,让我们继续往下看吧。

再看 script 部分

我们看这部分的时候,可能无从下手,但其实还是有点门道的。敲锣啦👏👏👏。。。。不管神马组件,都有三个较为重要的组成部分:propseventslot,这三个部分是组件对内对外沟通的桥梁,使得组件变得灵活起来。所以这三个 api 在发布之前一定构思好和确定好,因为后期再改就很难了,可能就是会牵一发动全身那样子。但后期对组件的处理其实不应该是这样的效果,而应该是不影响和改动之前的 api,但又可以扩展和新增功能。ok👌,就让我们一个一个娓娓道来吧👇。 首先看下 props 的部分,你需要在脑海中想象一下 button 组件的哪些内容是可变的(根据需要外部传参的改变而改变),不用着急往下看,先好好想一下💤。。。。 ... 1)最明显的就是 button 的背景色吧,这显然是可变的,就是 type; 2)然后是有没有图标,就是 icon; 3)还有就是有没有禁用,就是 disabled; 4)再来是有没有圆角,就是 round; 5)尺寸大小也是可变的吧,就是 size; 6)好像按钮还可以是文本的样子,就是 plain; .... 好了,那我们就试着写一下 props 部分吧!(注意:props 的部分最好用对象的写法,这样能够对每个属性进行自定义设置,相比数组的写法,更为规范严谨)。

<script>
export default {
  props: {
    type: {
      type: String,
      default: ''
    },
    size: {
      type: String,
      default: 'medium'
    },
    icon: {
      type: String,
      default: ''
    },
    disabled: Boolean,
    plain: Boolean,
    round: Boolean
  }
}
</script>

接下来是 slot 部分啦,如果不懂 slot 用法的同学可以先出门左拐学习一下再来✋。很明显,对于 button 组件来说,文本就是 slot 啦,所以 template 里面的内容可以小改一下,代码如下:

<template>
  <button class="el-button">
    <i></i>
    <span><slot></slot></span>
  </button>
</template>

然后是 event 部分,很显然啦,按钮能有什么功能呢,就是点击嘛,没了,所以它也就一个事件,就是当按钮被点击的时候,我们需要触发一个事件向上传递,也就是 $emit。于是乎,我们把事件添加到组件中,代码如下:

<template>
  <button
    class="el-button"
    @click="handleClick">
    <i></i>
    <span><slot></slot></span>
  </button>
</template>
<script>
export default {
  props: {
    ...
  },
  methods: {
    handleClick (e) {
      this.$emit('click', e);
    }
  }
}
</script>

好像 event 的部分就那么多,嗯,是的,比想象中的简单✊。。。。

再看 template 部分

你以为组件写完了,不,并没有,你不觉得 template 里面太空了么,而且 props 这部分的属性都还没用上呢(只是声明了一下),所以我们还需要完善点东西。。。 比如 slot 部分吧,通过 $slots.default 我们可以获取到 slot 中的内容,不过这里需要加个判断,因为用户可能没有传文字,那我们就不用渲染了; 又比如图标 i 的部分,和 slot 一样,有传值我们才渲染,所以也加个判断(这里 icon 的值为 el-icon-图标名 格式)。

<template>
  <button
    class="el-button"
    @click="handleClick">
    <i :class="icon" v-if="icon"></i>
    <span v-if="$slots.default"><slot></slot></span>
  </button>
</template>

再看 props 中的属性,其实当中大部分都是用来控制样式变化的,比如 typesizerounddisabledplain 等。。。所以就让我们为组件加上些 class 吧。

<template>
  <button
    class="el-button"
    @click="handleClick"
    :disabled="disabled"
    :class="[
      type ? 'el-button--' + type : '',
      size ? 'el-button--' + size : '',
      {
        'is-disabled': disabled,
        'is-plain': plain,
        'is-round': round
      }
    ]">
    <i :class="icon" v-if="icon"></i>
    <span v-if="$slots.default"><slot></slot></span>
  </button>
</template>

至此我们就写完了一个较为完整的 button 组件,是不是给人一种这么简单的么的感觉,虽然它还不够完善,但也覆盖了源码 90% 的部分,剩下的 10% 大家可以自己去补充补充。其实组件主要还是要看你🤔思考🤔得有多全面,想的越多写的越多。

怎么进行权限管理

日常问题

如何看待前端工程化?

目前来说,web业务日益复杂化和多元化,前端开发从WebPage模式为主转变为WebApp模式为主了。前端的开发工作在一些场景被认为只是日常的一项简单工作,或者只是某个项目的附属品,并没有被当作一个“软件”而认真对待。

在模式的转变下,前端都已经不是过去的拼几个页面和搞几个jq插件就能完成。当工程复杂就会产生很多问题,比如:

  • 如何进行高效的多人协作?
  • 如何保证项目的可维护性?
  • 如何提高项目的开发质量?
  • 如何降低项目生产的风险?

前端工程化是使用软件工程的技术和方法来进行前端的开发流程、技术、工具、经验等规范化、标准化,其主要目的是为了提高效率和降低成本,即提高开发过程中的开发效率,减少不必要的重复工作时间,而前端工程本质上是软件工程的一种,因此我们应该从软件工程的角度来研究前端工程。

“前端工程化”里面的工程指软件工程。

#如何做“前端工程化”? 前端工程化就是为了让前端开发能够自成体系,个人认为应该从模块化、组件化、规范化、自动化四个方面思考。

模块化

简单来说,模块化就是将一个大文件拆分成相互依赖的小文件,再进行统一的拼装和加载。

  • JS的模块化

在ES6之前,javascript一直没有模块系统,这对开发大型复杂的前端工程造成了巨大的障碍。对此社区制定了一些模块加载方案,如CommonJS、AMD和CMD等。

现在ES6已经在语言层面上规定了模块系统,完全可以取代现有的CommonJS和AMD规范,而且使用起来相当简洁,并且有静态加载的特性。

  1. 用webpack+babel将所有模块打包成一个文件同步加载,也可以搭乘多个chunk异步加载;
  2. 用system+babel主要是分模块异步加载;
  3. 用浏览器<script type="module">加载。
  • css的模块化

虽然sass、less、stylus等预处理器实现了css的文件拆分,但没有解决css模块化的一个重要问题:选择器的全局污染问题。

按道理,一个模块化的文件应该要隐藏内部作用域,只暴露少量接口给使用者。而按照目前预处理器的方式,导入一个css模块后,已存在的样式有被覆盖的风险。虽然重写样式是css的一个优势,但这并不利于多人协作。

为了避免全局选择器的冲突,需要制定css命名风格:

  1. ben风格
  2. bootstrap风格

从工具层面,社区又创造出Shadow DOM、CSS in JS和CSS Modules三种解决方案。

  1. Shadow DOM是webComponents的标准。它能解决全局污染问题,但目前很多浏览器不兼容,对我们来说还很久远。
  2. css in js是彻底抛弃css,使用js或者json来写样式。这种方法很激进,不能利用现有的css技术,而且处理伪类等问题比较困难;
  3. css modules仍然使用css,只是让js来管理依赖。它能够最大化地结合css生态和js模块化能力,目前来看是最好的解决方案。vue的scoped style也算是一种。
  • 资源的模块化

webpack的强大之处不仅仅在于它统一了js的各种模块系统,取代了browserify、requireJS、SeaJS的工作。更重要的是它的万能模块加载理念,即所有的资源都可以且也应该模块化。

资源模块化后,优点是:

  1. 依赖关系单一化。 所有css和图片等资源的依赖关系统一走js路线,无需额外处理css预处理器的依赖关系,也不需处理代码迁移时的图片合并、字体图片等路径问题;
  2. 资源处理集成化。 现在可以用loader对各种资源做各种事情,比如复杂的vue-loader等等;
  3. 项目结构清晰化。 使用webpack后,你的项目结构总可以表示成这样的函数:dest=webpack(src, config)

组件化

从ui拆分下来的每个包含模板(html)+样式(css)+逻辑(js)功能完备的结构单元,我们称之为组件。

组件化≠模块化。模块化只是在文件层面上,对代码或资源的拆分;而组件化是在设计层面上,对ui(用户界面)的拆分。

其实,组件化更重要是一种分治思想。

页面上所有的东西都是组件。页面是个大型组件,可以拆成若干个中型组件,然后中型组件还可以再拆,拆成若干个小型组件,小型组件也可以再拆,直到拆成dom元素为止。dom元素可以看成是浏览器自身的组件,作为组件的基本单元。

传统前端框架/类库的思想是先组织dom,然后把某些可服用的逻辑封装成组件来操作dom,是dom优先;而组件化框架/类库的思想是先来构思组件,然后用dom这种基本单元结合相应逻辑来实现组件,是组件优先。这是两者本质的区别。

其次,组件化实际是一种按照模板(html)+样式(css)+逻辑(js)三位一体的形式对面向对象的进一步抽象。

所以我们除了封装组件本身,还要合理处理组件之间的关系,比如(逻辑)继承、(样式)扩展、(模板)嵌套和包含等,这些关系都可以归为依赖。

目前市面上的组件化框架很多,主要有vue、react、angular。

规范化

规范化其实是工程化中很重要的一个部分,项目初期规范制定的好坏会直接影响到后期的开发质量。

比如:

  1. 目录结构的制定
  2. 编码规范
  3. 前后端接口规范
  4. 文档规范
  5. 组件管理
  6. git分支管理
  7. commit描述规范
  8. 视觉图表规范
  9. ……

自动化

前端工程化的很多脏活累活都应该交给自动化工具来完成。

  1. 图标合并
  2. 持续继承
  3. 自动化构建
  4. 自动化部署
  5. 自动化测试

git工作流 - 如何提交代码? Node中间层 - 用于渲染一部分模板和路由等。 CI/CD - 主要利用git hooks通知CI,执行对应的脚本(如gitlab)。 监控 - 前端监控主要分为性能监控和业务监控,它应支持自由配置各种报表和一系列报警规则。

在项目开发过程之前是否有提前进行一些工程化和模块化的分工?

分工安排主要包含以下内容: 1.公共组件(包括common.css 和 common.js) 一人维护,各子频道专人负责,每个频道正常情况下由一人负责,要详细写明注释,如果多人合作,维护的人员注意添加注释信息。 2.视觉设计师设计完设计图后,先后交互设计师沟通,确定设计可行,然后先将设计图给公共组件维护者,看设计图是否需要提取公共组件,然后再提交给相应频道的前端工程师。如果有公共组件要提取,公共组件维护者需对频道前端工程师说明。 3.如果确定没有公共组件需提取,交互设计师直接和各栏目的前端工程师交流,对照着视觉设计师的设计图进行需求说明,前端工程师完成需求。 4.前端工程师在制作页面时,需先去common 文件中查询是否已经存在设计图中的组件,如果有,直接调用;如果没有,则在app.css 和 app.js 中添加相应的代码(app指各频道自己的文件)。 5.前端工程师在制作过程中,发现高度重用的模块,却未被加入到公共组件中,需向公共组件维护人员进行说明,然后公共组件维护人员决定是否添加该组件。如果确定添加,则向前端工程师说明添加了新组件,让前端工程师检查之前是否添加了类似的组件,统一更新成新组件的用法,删除之前自定义的css 和js。虽然麻烦,但始终把可维护性放在首位。 6.公共组件维护者的公共组件说明文档,需提供配套的图片和说明文字,方便阅读。

在组件化的过程中,你觉得什么样的组件是一个比较好的组件?什么样的组件是高复用性的组件?

flex布局,还问项目一般用什么布局?

一、静态布局(Static Layout)

1. 布局概念

最传统、原始的Web布局设计。网页最外层容器(outer)有固定的大小,所有的内容以该容器为标准,超出宽高的部分用滚动条(overflow:scroll)来实现滚动查阅。

2. 优点

采用的是css2之前的写法,不存在浏览器兼容性。布局简单。

3. 缺点

但是移动端不可以使用pc端的页面,两个页面的布局不一致,移动端需要自己另外设计一个布局并使用不同域名呈现。

4. 实现方法

PC端: 最外层居中,使用固定的宽(高)度,超出部分用滚动条查阅。 例如百度首页外层body设置了一个min-width:1000px;,当我打开调试器的时候,底部x轴滚动条就出现了。

移动端 由于静态布局不适用于手机端,所以一般都会另设计一个布局,并使用另一个域名。

再看一下最近比较'火'的京东的案例:分别访问

  • jd.com
  • m.jd.com

可以发现: PC端限制了最小的宽度, 低于了则以最小宽度出现滚动条 移动端限制了最大的宽度, 超过了则以最大宽度居中显示如刚刚百度的PC端我们切换成手机模拟器访问试试:

二、流式布局(Liquid Layout)

1. 布局概念

流式布局也叫百分比布局

这边引入一下自适应布局: 分别为不同的屏幕设置布局格式,当屏幕大小改变时,会出现不同的布局,意思就是在这个屏幕下这个元素块在这个地方,但是在那个屏幕下,这个元素块又会出现在那个地方。只是布局改变,元素不变。可以看成是不同屏幕下由多个静态布局组成的。

而流式布局的特点是随着屏幕的改变,页面的布局没有发生大的变化,可以进行适配调整,这个正好与自适应布局相补。

流式布局常用的设计模板: 左侧固定+右侧自适应 左右固定宽度+中间自适应(参考京东手机版)

页面元素的宽度按照屏幕进行适配调整,主要的问题是如果屏幕尺度跨度太大,那么在相对其原始设计而言过小或过大的屏幕上不能正常显示 。 你看到的页面,元素的大小会变化而位置不会变化——这就导致如果屏幕太大或者太小都会导致元素无法正常显示。

2. 优点

元素的宽高用百分比做单位,元素宽高按屏幕分辨率调整,布局不发生变化

3. 缺点

屏幕尺度跨度过大的情况下,页面不能正常显示。

三、响应式布局(Responsive layout)

采用自适应布局和流式布局的综合方式,为不同屏幕分辨率范围创建流式布局

现在优秀的页面都追求一套代码可以实现三端的浏览; 从概念可以看出来,自适应布局的诞生是为了实现不同屏幕分辨率的终端上浏览网页的不同展示方式。

通过响应式设计能使网站在手机和平板电脑上有更好的浏览阅读体验。屏幕尺寸不一样展示给用户的网页内容也不一样.

利用媒体查询可以检测到屏幕的尺寸(主要检测宽度),并设置不同的CSS样式,就可以实现响应式的布局。

大名鼎鼎的bootstrap就是响应式布局的专家。

官方放出狠话: Bootstrap 提供了一套响应式、移动设备优先的流式栅格系统,随着屏幕或视口(viewport)尺寸的增加,系统会自动分为最多12列。它包含了易于使用的预定义类,还有强大的mixin 用于生成更具语义的布局。

连我们最热爱的React官方也热衷于响应式布局设计:

平常管理GitHub吗?用过哪些Git指令?Git的流程和原理以及对应的指令都是些什么呢?

在实际开发中,会使用git作为版本控制工具来完成团队协作。因此,对基本的git操作指令进行总结是十分有必要的,本文对一些术语或者理论基础,不重新码字,可以参考廖雪峰老师的博文,本文只对命令做归纳总结。

git总结

工作中使用

1.基本原理和使用

git的通用操作流程如下图(来源于网络)

git操作通用流程

主要涉及到四个关键点:

  1. 工作区:本地电脑存放项目文件的地方,比如learnGitProject文件夹;
  2. 暂存区(Index/Stage):在使用git管理项目文件的时候,其本地的项目文件会多出一个.git的文件夹,将这个.git文件夹称之为版本库。其中.git文件夹中包含了两个部分,一个是暂存区(Index或者Stage),顾名思义就是暂时存放文件的地方,通常使用add命令将工作区的文件添加到暂存区里;
  3. 本地仓库:.git文件夹里还包括git自动创建的master分支,并且将HEAD指针指向master分支。使用commit命令可以将暂存区中的文件添加到本地仓库中;
  4. 远程仓库:不是在本地仓库中,项目代码在远程git服务器上,比如项目放在github上,就是一个远程仓库,通常使用clone命令将远程仓库拷贝到本地仓库中,开发后推送到远程仓库中即可;

更细节的来看:

git几个核心区域间的关系

日常开发时代码实际上放置在工作区中,也就是本地的XXX.java这些文件,通过add等这些命令将代码文教提交给暂存区(Index/Stage),也就意味着代码全权交给了git进行管理,之后通过commit等命令将暂存区提交给master分支上,也就是意味打了一个版本,也可以说代码提交到了本地仓库中。另外,团队协作过程中自然而然还涉及到与远程仓库的交互。

因此,经过这样的分析,git命令可以分为这样的逻辑进行理解和记忆:

  1. git管理配置的命令;

    几个核心存储区的交互命令:

  2. 工作区与暂存区的交互;

  3. 暂存区与本地仓库(分支)上的交互;

  4. 本地仓库与远程仓库的交互。

2. git配置命令

查询配置信息

  1. 列出当前配置:git config --list;
  2. 列出repository配置:git config --local --list;
  3. 列出全局配置:git config --global --list;
  4. 列出系统配置:git config --system --list;

第一次使用git,配置用户信息

  1. 配置用户名:git config --global user.name "your name";
  2. 配置用户邮箱:git config --global user.email "youremail@github.com";

其他配置

  1. 配置解决冲突时使用哪种差异分析工具,比如要使用vimdiff:git config --global merge.tool vimdiff;
  2. 配置git命令输出为彩色的:git config --global color.ui auto;
  3. 配置git使用的文本编辑器:git config --global core.editor vi;

3. 工作区上的操作命令

新建仓库

  1. 将工作区中的项目文件使用git进行管理,即创建一个新的本地仓库:git init
  2. 从远程git仓库复制项目:git clone <url>,如:git clone git://github.com/wasd/example.git;克隆项目时如果想定义新的项目名,可以在clone命令后指定新的项目名:git clone git://github.com/wasd/example.git mygit

提交

  1. 提交工作区所有文件到暂存区:git add .
  2. 提交工作区中指定文件到暂存区:git add <file1> <file2> ...;
  3. 提交工作区中某个文件夹中所有文件到暂存区:git add [dir];

撤销

  1. 删除工作区文件,并且也从暂存区删除对应文件的记录:git rm <file1> <file2>;
  2. 从暂存区中删除文件,但是工作区依然还有该文件:git rm --cached <file>;
  3. 取消暂存区已经暂存的文件:git reset HEAD <file>...;
  4. 撤销上一次对文件的操作:git checkout --<file>。要确定上一次对文件的修改不再需要,如果想保留上一次的修改以备以后继续工作,可以使用stashing和分支来处理;
  5. 隐藏当前变更,以便能够切换分支:git stash
  6. 查看当前所有的储藏:git stash list
  7. 应用最新的储藏:git stash apply,如果想应用更早的储藏:git stash apply stash@{2};重新应用被暂存的变更,需要加上--index参数:git stash apply --index;
  8. 使用apply命令只是应用储藏,而内容仍然还在栈上,需要移除指定的储藏:git stash drop stash{0};如果使用pop命令不仅可以重新应用储藏,还可以立刻从堆栈中清除:git stash pop;
  9. 在某些情况下,你可能想应用储藏的修改,在进行了一些其他的修改后,又要取消之前所应用储藏的修改。Git没有提供类似于 stash unapply 的命令,但是可以通过取消该储藏的补丁达到同样的效果:git stash show -p stash@{0} | git apply -R;同样的,如果你沒有指定具体的某个储藏,Git 会选择最近的储藏:git stash show -p | git apply -R

更新文件

  1. 重命名文件,并将已改名文件提交到暂存区:git mv [file-original] [file-renamed];

查新信息

  1. 查询当前工作区所有文件的状态:git status;
  2. 比较工作区中当前文件和暂存区之间的差异,也就是修改之后还没有暂存的内容:git diff;指定文件在工作区和暂存区上差异比较:git diff <file-name>;

4. 暂存区上的操作命令

提交文件到版本库

  1. 将暂存区中的文件提交到本地仓库中,即打上新版本:git commit -m "commit_info";
  2. 将所有已经使用git管理过的文件暂存后一并提交,跳过add到暂存区的过程:git commit -a -m "commit_info";
  3. 提交文件时,发现漏掉几个文件,或者注释写错了,可以撤销上一次提交:git commit --amend;

查看信息

  1. 比较暂存区与上一版本的差异:git diff --cached;
  2. 指定文件在暂存区和本地仓库的不同:git diff <file-name> --cached;
  3. 查看提交历史:git log;参数-p展开每次提交的内容差异,用-2显示最近的两次更新,如git log -p -2;

打标签

Git 使用的标签有两种类型:轻量级的(lightweight)和含附注的(annotated) 。轻量级标签就像是个不会变化的分支,实际上它就是个指向特定提交对象的引用。而含附注标签,实际上是存储在仓库中的一个独立对象,它有自身的校验和信息,包含着标签的名字,电子邮件地址和日期,以及标签说明,标签本身也允许使用 GNU Privacy Guard (GPG) 来签署或验证。一般我们都建议使用含附注型的标签,以便保留相关信息;当然,如果只是临时性加注标签,或者不需要旁注额外信息,用轻量级标签也没问题。

  1. 列出现在所有的标签:git tag;
  2. 使用特定的搜索模式列出符合条件的标签,例如只对1.4.2系列的版本感兴趣:git tag -l "v1.4.2.*";
  3. 创建一个含附注类型的标签,需要加-a参数,如git tag -a v1.4 -m "my version 1.4";
  4. 使用git show命令查看相应标签的版本信息,并连同显示打标签时的提交对象:git show v1.4;
  5. 如果有自己的私钥,可以使用GPG来签署标签,只需要在命令中使用-s参数:git tag -s v1.5 -m "my signed 1.5 tag";
  6. 验证已签署的标签:git tag -v ,如git tag -v v1.5;
  7. 创建一个轻量级标签的话,就直接使用git tag命令即可,连-a,-s以及-m选项都不需要,直接给出标签名字即可,如git tag v1.5;
  8. 将标签推送到远程仓库中:git push origin ,如git push origin v1.5
  9. 将本地所有的标签全部推送到远程仓库中:git push origin --tags;

分支管理

  1. 创建分支:git branch <branch-name>,如git branch testing
  2. 从当前所处的分支切换到其他分支:git checkout <branch-name>,如git checkout testing
  3. 新建并切换到新建分支上:git checkout -b <branch-name>;
  4. 删除分支:git branch -d <branch-name>
  5. 将当前分支与指定分支进行合并:git merge <branch-name>;
  6. 显示本地仓库的所有分支:git branch;
  7. 查看各个分支最后一个提交对象的信息:git branch -v;
  8. 查看哪些分支已经合并到当前分支:git branch --merged;
  9. 查看当前哪些分支还没有合并到当前分支:git branch --no-merged;
  10. 把远程分支合并到当前分支:git merge <remote-name>/<branch-name>,如git merge origin/serverfix;如果是单线的历史分支不存在任何需要解决的分歧,只是简单的将HEAD指针前移,所以这种合并过程可以称为快进(Fast forward),而如果是历史分支是分叉的,会以当前分叉的两个分支作为两个祖先,创建新的提交对象;如果在合并分支时,遇到合并冲突需要人工解决后,再才能提交;
  11. 在远程分支的基础上创建新的本地分支:git checkout -b <branch-name> <remote-name>/<branch-name>,如git checkout -b serverfix origin/serverfix;
  12. 从远程分支checkout出来的本地分支,称之为跟踪分支。在跟踪分支上向远程分支上推送内容:git push。该命令会自动判断应该向远程仓库中的哪个分支推送数据;在跟踪分支上合并远程分支:git pull
  13. 将一个分支里提交的改变移到基底分支上重放一遍:git rebase <rebase-branch> <branch-name>,如git rebase master server,将特性分支server提交的改变在基底分支master上重演一遍;使用rebase操作最大的好处是像在单个分支上操作的,提交的修改历史也是一根线;如果想把基于一个特性分支上的另一个特性分支变基到其他分支上,可以使用--onto操作:git rebase --onto <rebase-branch> <feature branch> <sub-feature-branch>,如git rebase --onto master server client;使用rebase操作应该遵循的原则是:一旦分支中的提交对象发布到公共仓库,就千万不要对该分支进行rebase操作

5.本地仓库上的操作

  1. 查看本地仓库关联的远程仓库:git remote;在克隆完每个远程仓库后,远程仓库默认为origin;加上-v的参数后,会显示远程仓库的url地址;
  2. 添加远程仓库,一般会取一个简短的别名:git remote add [remote-name] [url],比如:git remote add example git://github.com/example/example.git;
  3. 从远程仓库中抓取本地仓库中没有的更新:git fetch [remote-name],如git fetch origin;使用fetch只是将远端数据拉到本地仓库,并不自动合并到当前工作分支,只能人工合并。如果设置了某个分支关联到远程仓库的某个分支的话,可以使用git pull来拉去远程分支的数据,然后将远端分支自动合并到本地仓库中的当前分支;
  4. 将本地仓库某分支推送到远程仓库上:git push [remote-name] [branch-name],如git push origin master;如果想将本地分支推送到远程仓库的不同名分支:git push <remote-name> <local-branch>:<remote-branch>,如git push origin serverfix:awesomebranch;如果想删除远程分支:git push [romote-name] :<remote-branch>,如git push origin :serverfix。这里省略了本地分支,也就相当于将空白内容推送给远程分支,就等于删掉了远程分支。
  5. 查看远程仓库的详细信息:git remote show origin
  6. 修改某个远程仓库在本地的简称:git remote rename [old-name] [new-name],如git remote rename origin org
  7. 移除远程仓库:git remote rm [remote-name]

6. 忽略文件.gitignore

一般我们总会有些文件无需纳入 Git 的管理,也不希望它们总出现在未跟踪文件列表。通常都是些自动生成的文件,比如日志文件,或者编译过程中创建的临时文件等。我们可以创建一个名为 .gitignore 的文件,列出要忽略的文件模式。如下例:

# 此为注释 – 将被 Git 忽略
# 忽略所有 .a 结尾的文件
*.a
# 但 lib.a 除外
!lib.a
# 仅仅忽略项目根目录下的 TODO 文件,不包括 subdir/TODO
/TODO
# 忽略 build/ 目录下的所有文件
build/
# 会忽略 doc/notes.txt 但不包括 doc/server/arch.txt
doc/*.txt
# 忽略 doc/ 目录下所有扩展名为 txt 的文件
doc/**/*.txt

在前端常用的debug的手段?chrome的哪些部分分别能看到什么方面?

img

前端开发调试最佳实践

B端页面?页面优化的方法?

是否了解过正则表达式?用来做什么?

preview

最初见到正则表达式是在表单验证里,多少会用些 validate 的库,基本的电话 / 邮箱之类的校验都有现成的,真正自己写正则去校验输入格式的机会并不多

1、老项目迁移,所有的 T.dom.getElementById('abc') 代码都要改成新的写法 $('#abc')

2、组件库升级,所有的 <el-dialog v-model="a" 必须改成 <el-dialog :visible.sync="a"

都是真实工作中的脏活累活,故事 1 中的项目有近 100 个页面,由于 T 库弃用了,不仅 T.dom.getElementById 还有 getElementByClass 等等调用都要改成 jquery 的写法。如果完全靠人肉,那是多么的苦力。

故事 2 中的组件库其实就是我们的 Element,我们原先很多项目都是 Element 1.x,要升级到 2.x,这个对话框的 breaking change 影响还挺大的,在 2.x 中通过 v-model 是无法唤起对话框的。因此要确保每个 el-dialog 都检查一遍,而模板代码里 el-dialogv-model 可能不在第一个,属性多的时候还会换行,都需要火眼金睛。

聪明的读者肯定知道,靠人肉是个没有办法的办法,而且看多了也会眼花,最好还要 double check。虽然写正则表达式去找,也不能保证 100% 都覆盖,毕竟老项目里各种迷之代码都有,但正则能帮我们找出大部分,并且 replace 的时候也能避免输入错误,这样可以把精力放在 double check 上。

表单校验

url参数提取

引号的替换

字符串去重替换

定制 .vue 单文件模板

最近在做微信小程序,每个页面都必须写 wxml / wxss / js / json 这 4 个文件,当项目里页面多的时候文件就巨多无比。假如没有用任何开发框架,可以自己定制一个单文件模板,有点类似 .vue 文件

现在我们的目标是把这个文件拆成模板、样式、js 和 json 配置对应的 4 个文件,抛弃原来的 split 大法或者逐行读文件,正则表达式可以帮我们优雅地解决问题。

你是否了解或使用过eslint?

我们在前端工程化中可以这样使用 ESLint:

  1. 基于业界现有的 ESLint 规范和团队代码习惯定制一套统一的 ESLint 代码规则
  2. 将统一代码规则封装成 ESLint 规则包接入
  3. 将 ESLint 接入脚手架、编辑器以及研发工作流中

你知道你的项目是怎么从变成一个html文件的吗?

HtmlWebpackPlugin

html-webpack-plugin 的作用是:当使用 webpack打包时,创建一个 html 文件,并把 webpack 打包后的静态文件自动插入到这个 html 文件当中。

安装

npm install html-webpack-plugin --save-dev

使用默认配置

const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
  entry: 'index.js',
  output: {
    path: __dirname + '/dist',
    filename: 'index_bundle.js'
  },
  plugins: [
    new HtmlWebpackPlugin()
  ]
}

html-webpack-plugin 默认将会在 output.path 的目录下创建一个 index.html 文件, 并在这个文件中插入一个 script 标签,标签的 srcoutput.filename

生成的文件如下:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Webpack App</title>
  </head>
  <body>
    <script src="bundle.js"></script>
  </body>
</html>

当配置多个入口文件 entry 时, 生成的将都会使用 script 引入。

如果 webpack 的输出中有任何CSS资源 (例如,使用 mini-css-extract-plugin 提取的 CSS),那么这些资源将包含在 HTML 头部的 link 标记中。

更多配置

在实际的项目中,需要自定义一些 html-webpack-plugin 的配置, 像指定生成目录和文件, 使用指定模版生成文件, 更改 document.title 信息等, 这就更改默认配置来实现。

属性名字段类型默认值说明
titleStringWebpack App网页 document.title 的配置, 在index.html 文件中可以使用 <%= htmlWebpackPlugin.options.title %> 设置网页标题为这里设置的值。
filenameStringindex.htmlhtml 文件生成的名称,可以使用 assets/index.html 来指定生成的文件目录和文件名, 重点1:生成文件的跟路径为ouput.path的目录。 重点2: ‘assets/index.html’ 和 ./assets/index.html 这两种方式的效果时一样的, 都是在 output.path 目录下生成 assets/index.html
templateString生成 filename 文件的模版, 如果存在 src/index.ejs, 那么默认将会使用这个文件作为模版。 重点:与 filename 的路径不同, 当匹配模版路径的时候将会从项目的跟路径开始
templateParametersBooleanObjectFunction覆盖默认的模版中使用的参数
injectBooleanStringtrue制定 webpack 打包的 js css 静态资源插入到 html 的位置, 为 true 或者 body 时, 将会把 js 文件放到 body 的底部, 为 head 时, 将 js 脚本放到 head 元素中。
faviconString为生成的 html 配置一个 favicon
meteObject{}为生成的 html 文件注入一些 mete 信息, 例如: {viewport: 'width=device-width, initial-scale=1, shrink-to-fit=no'}
baseObjectStringfalsefalse在生成文件中注入 base 标签, 例如 base: "https://example.com/path/page.html <base> 标签为页面上所有的链接规定默认地址或默认目标
minifyBooleanObject如果 mode 设置为 production 默认为 true 否则设置为 false设置静态资源的压缩情况
hashBooleanfalse如果为真,则向所有包含的 jsCSS 文件附加一个惟一的 webpack 编译散列。这对于更新每次的缓存文件名称非常有用
cacheBooleantrue设置 js css 文件的缓存,当文件没有发生变化时, 是否设置使用缓存
showErrorsBooleantrue当文件发生错误时, 是否将错误显示在页面
xhtmlBooleanfalse当设置为 true 的时候,将会讲 <link> 标签设置为符合 xhtml 规范的自闭合形式

属性的使用方法

webpack.config.js

{
  entry: 'index.js',
  output: {
    path: __dirname + '/dist', 
    filename: 'bundle.js'
  },
  plugins: [
    new HtmlWebpackPlugin({
      title: 'My App', 
      filename: 'assets/admin.html'  // 在  output.path 目录下生成 assets/admin.html 文件
    })
  ]
}

生成多个 html 文件

生成多个 html 文件只需要多次在 plugins 中使用 HtmlWebpackPlugin webpack.config.js

{
  entry: 'index.js',
  output: {
    path: __dirname + '/dist', 
    filename: 'bundle.js'
  },
  plugins: [
    new HtmlWebpackPlugin(),
    new HtmlWebpackPlugin({
      title: 'My App', 
      filename: 'assets/admin.html'  // 在  output.path 目录下生成 assets/admin.html 文件
    })
  ]
}

使用自定义模版生成 html 文件

如果默认的 html 模版不能满足业务需求, 比如需要蛇生成文件里提前写一些 css 'js' 资源的引用, 最简单的方式就是新建一个模版文件, 并使用 template 属性指定模版文件的路径,html-webpack-plugin 插件将会自动向这个模版文件中注入打包后的 js 'css' 文件资源。

webpack.config.js

plugins: [
  new HtmlWebpackPlugin({
    title: 'My App', 
    template: 'public/index.html'
  })
]

public/index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8"/>
    <title><%= htmlWebpackPlugin.options.title %></title>
    <link src="xxx/xxx.css">
  </head>
  <body>
  </body>
</html>

使用自定义的模版接收 HtmlWebpackPlugin 中定义的 title 需要使用 <%= htmlWebpackPlugin.options.title %>

Minification

如果 minify 选项设置为 true (webpack模式为 production 时的默认值),生成的 HTML 将使用 HTML-minifier 和以下选项进行压缩:

{
  collapseWhitespace: true,
  removeComments: true,
  removeRedundantAttributes: true,
  removeScriptTypeAttributes: true,
  removeStyleLinkTypeAttributes: true,
  useShortDoctype: true
}

若要使用自定义 html 压缩器选项,请传递一个对象来配置。此对象不会与上面的默认值合并。

若要在生产模式期间禁用 minification,请将 minify 选项设置为 false

部署一个网站需要哪些流程呢? 云服务用过哪些

正儿八经的前端项目部署流程

脚手架是怎么实现的

文章

第一个版本的功能比较简单,大致为:

  1. 用户输入命令,准备创建项目。
  2. 脚手架解析用户命令,并弹出交互语句,询问用户创建项目需要哪些功能。
  3. 用户选择自己需要的功能。
  4. 脚手架根据用户的选择创建 package.json 文件,并添加对应的依赖项。
  5. 脚手架根据用户的选择渲染项目模板,生成文件(例如 index.htmlmain.jsApp.vue 等文件)。
  6. 执行 npm install 命令安装依赖。

├─.vscode ├─bin │ ├─mvc.js # mvc 全局命令 ├─lib │ ├─generator # 各个功能的模板 │ │ ├─babel # babel 模板 │ │ ├─linter # eslint 模板 │ │ ├─router # vue-router 模板 │ │ ├─vue # vue 模板 │ │ ├─vuex # vuex 模板 │ │ └─webpack # webpack 模板 │ ├─promptModules # 各个模块的交互提示语 │ └─utils # 一系列工具函数 │ ├─create.js # create 命令处理函数 │ ├─Creator.js # 处理交互提示 │ ├─Generator.js # 渲染模板 │ ├─PromptModuleAPI.js # 将各个功能的提示语注入 Creator └─scripts # commit message 验证脚本 和项目无关 不需关注

前端实现:怎么把点绘制成线

两个栈实现队列,怎么做?