浏览器排序对比-Chrome VS Firefox

作者:颜亦浠@毛豆前端

Chrome浏览器和Firefox浏览器对一些算法的实现存在差异,以前遇到过一个问题,做了一个记录,下面就和大家一起探讨下。

一、问题描述

接口返回数据相同,经过排序后,同一个页面两个浏览器展示效果不同。

二、问题定位

1、前端排序算法不对

2、其他因素影响

重新梳理后,发现sort用法写的不对。

sort()如果不带参数,就是按照字母顺序对数组中的元素进行排序,也就是按照字母编码的顺序进行排序。

如果sort()中传入排序规则函数,则可以自定义排序。

规则如下:

1、比较函数应该有两个参数a,b

2、 若 a 小于 b,在排序后的数组中 a 应该出现在 b 之前,则返回一个小于 0 的值。若 a 等于 b,则返回 0。若 a 大于 b,则返回一个大于 0 的值。

修改之后在打断点验证的过程中发现,两个浏览器遍历过程中每一次的结果是不同的,虽然最终排序的结果一致。

深究发现,两个浏览器内核在进行排序是所采用的算法不同。

火狐sort排序用的是归并排序,chrome浏览器用的是插入排序、快速排序(数量小于10的数组使用 InsertionSort,比10大的数组则使用 QuickSort)

三、几种算法的基本实现以及对比

1、归并排序(Merge Sort)

基本思想:将两个有序数列合并成一个有序数列,包括从上往下、从下往上两种方式,区别在于分组大小,前者倾向于首先均分为两个组,后者倾向于分成每组只有一个元素的多个组。

基本demo:

   // 归并排序的一般实现
    const merge = (left, right) => {
        const result = [];
        while (left.length && right.length) {
            if(left[0] <= right[0]) {
                result.push(left.shift()); 
            } else {
                result.push(right.shift())
            }
        }
        while(left.length) result.push(left.shift());
        while(right.length) result.push(right.shift());
        return result;
    }
    const mergeSort = arr => {
        const len = arr.length;
        if (len < 2) {
            return arr;
        }
        let middle = Math.floor(len/2);
        let left = arr.slice(0, middle);
        let right = arr.slice(middle);
        return merge(mergeSort(left), mergeSort(right))
    }

2、快速排序(Quick Sort)

基本思想:在数组中选择一个基数,比基数大的放在右边子数组,比基数小的放在左边子数组,然后在分别对左右两边的数组分别执行以上操作,直到所分成的数组都只有一个元素。

基本demo:

    //快速排序的一般实现
    const quickSort1 = arr => {
    if (arr.length <= 1) {
        return arr;
    }
    const midIndex = Math.floor(arr.length / 2);
    const valArr = arr.splice(midIndex, 1);
    const midIndexVal = valArr[0];
    const left = []; 
    const right = []; 
    for (let i = 0; i < arr.length; i++) {
        if (arr[i] < midIndexVal) {
            left.push(arr[i]);
        } else {
            right.push(arr[i]);
        }
    }
    return quickSort1(left).concat(midIndexVal, quickSort1(right));
};

3、插入排序(Insertion Sort)

比较适用于大部分元素已经排序好的情况。

基本思想:把整个数组拆分成两个子数组,一个为按顺序排好的,一个为没有排序的,每次从没有排序的数组中拆除一个数,将起放入已经排序好的数组中并排好序,直到没有排序的数组为空为止。

基本demo:

    //插入排序的一般实现
    const insertionSort = (nums) => {
        for (let i = 1; i < nums.length; ++i) {
            let preIndex = i -1;
            let temp = nums[i];
            while (j >=0 && nums[preIndex] > temp) {
                nums[preIndex+1] = nums[preIndex];
                preIndex--;
            }
            nums[preIndex+1] = temp;
        }
        return nums;
    }

四、性能分析

时间复杂度:

  • 归并排序(O(nlogn))
  • 快速排序(O(nlogn))
  • 插入排序(O(n²))

稳定性:

  • 归并排序(稳定)
  • 快速排序(不稳定)
  • 插入排序(稳定)

优化方案:

  • 插入排序:借助二分查找的折半插入
    const binarySearch = (arr, maxIndex, value) => {
          let min = 0;
          let max = maxIndex;
          while (min <= max) {
              const mid = Math.floor((min + max) / 2);
              if (arr[mid] <= value) {
                  min = mid + 1;
              } else {
                  max = mid - 1;
              }
          }
          return min;
      }
    const insertionSort2 = (arr) => {
          for (let i = 1, len = arr.length; i < len; i++) {
              const temp = arr[i];
              const insertIndex = binarySearch(arr, i - 1, arr[i]);
              for (let preIndex = i - 1; preIndex >= insertIndex; preIndex--) {
                  arr[preIndex + 1] = arr[preIndex];
              }
              arr[insertIndex] = temp;
          }
          return arr;
      }
    
  • 归并排序:空间优化,用array.splice 取代 array.slice,减少空间消耗;对其中规模较小的子数组,使用插入排序;
  • 快速排序:in-place
 const swap = (arr, i, j) => {
        let temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    };
    const partition = (arr, left, right) => {
        let pivot = left, //设定基准值(pivot)
        index = pivot + 1;
        for (let i = index; i <= right; i++) {
            if (arr[i] < arr[pivot]) {
                swap(arr, i, index);
                index++;
            }
        }
        swap(arr, pivot, index - 1);
        return index - 1;
    }; 
    const quickSort = (arr, left, right) => {
        let len = arr.length, index;
        left = typeof left != 'number' ? 0 : left;
        right = typeof right != 'number' ? len-1 : right;
        if (left < right ) {
            index = partition(arr, left, right);
            quickSort(arr, left, index - 1);
            quickSort(arr, index + 1, right);
        }
        return arr;
    }

前端性能优化之初解析

作者:鱼某某@毛豆前端

想做前端性能优化,就要知道从输入url,按下回车那一刻,到页面呈现在你面前这个过程到底经历了什么? 1.png

从上图中我们可以总结为4点:

1、DNS解析次数

2、TCP连接接时间

3、Http请求和响应

4、浏览器渲染

下面,我们将性能优化分为请求速度优化和渲染优化两部分来讲:

1、请求速度优化

资源从请求发出到接受这个部分要经历,DNS解析,TCP连接,请求和服务端响应这几个部分,并且这些部分都是要花费时间的,下面我们依次来讲述如何减少各个节点的时间:

DNS解析

说到DNS解析,我们能做的就是将解析过程前置,就是俗称的预解析,DNS解析后会缓存,待真正请求资源时,就不会再花费DNS解析的时间。

注:浏览器会将我们页面中使用的域名自动进行DNS解析,所以我们只需配置页面中没有出现的域名即可,但是要注意的是,要合理配置dns的解析,否则会浪费一些不必要的资源 image.png

TCP连接

讲解如何减少TCP连接次数之前,先来复习下三次握手和四次挥手: TCP三次握手🤝

Https比Http多了SSL解析(第四次握手🤝)

image.png

TCP四次挥手👋

从上图可以看到这个每次http请求所要连接和断开的时间远远比我们想象的要多,但是页面中避免不开http的请求,所以下面我们就要来说说如何减少tcp的连接次数:

1、Keep-alive

2、握手复用 3、减少http请求

4、http2戳我👇

减少http请求我们常用的方法有:

1、合并静态资源

2、缓存

3、本地存储

缓存分为Memory cache、Disk cache、Servivce worker cache三种: image.png

具体如何设置呢,打开控制台,仔细看一看请求头中的字段,可以总结为下图: image.png

服务端响应

tcp建立连接后,就要面对静态资源传输速度的问题,从传输这两个字来看,肯定会和传输大小和传输距离有关,所以我们就要考虑如何将静态资源变的小,传输距离变得短?

1、Gzip image.png 2、构建工具缩小js,css文件大小

3、CDN

CDN域名我们需要注意一下下,它和我们的域名是不一样的哦,这样它就不需要携带cookie,这样也变相的减少了请求头的大小 image.png 4、选择合理的图片类型

其他

多域名,利用浏览器的并发性,浏览器一次可以请求同域名下4-6个静态资源,所以在遇到阻塞的情况,不同域名下的静态资源则不会收到影响,所以可以利用多域名来加载静态资源,但需要注意的是,不要设置过多域名,会造成性能浪费,3-4个就好

2、浏览器渲染优化

image.png

众所周知页面解析时遇到link,js标签会停下等待资源的加载,并且为了防止浏览器裸奔,render tree的形成很重要,所以css放在头部,js放在尾部是必然的

减少重绘和回流的次数

重绘:当render tree中的一些元素需要更新属性,而这些属性只是影响元素的外观,风格,而不会影响布局的,比如background-color,则称为重绘

回流:当render tree中的一部分(或全部)因为元素的规模尺寸,布局,隐藏等改变而需要重新构建,这就称为回流 注意:回流必将引起重绘,而重绘不一定会引起回流

回流何时发生:

1、添加或者删除可见的DOM元素

2、元素位置改变

3、当页面布局或几何属性改变——边距、填充、边框、宽度和高度

4、内容改变——比如文本改变或者图片大小改变而引起的计算值宽度和高度改变

5、页面渲染初始化

6、浏览器窗口尺寸改变——resize事件发生时

7、获取offsetTop/Left/Width/Height , scrollTop/Left/Width/Height , clientTop/Left/Width/Height,Width/Height

所以:

1、用class来改变样式

2、脱离文档流,display:none或者position:absolute/fixed

3、dom结构离线处理后一次插入

4、不要经常访问会引起浏览器flush队列的属性,如果你确实要访问,利用缓存

懒加载

原则就是将必须呈现的东西快速加载,其余的资源可以按需加载,让用户快速看到页面,减少等待时间

1、白屏时间(从按下回车到第一个元素出现的时间)

2、首屏时间(第一屏展示出的时间) image.png

总结

极端的总结为一句话😁:请求能少发就少发,资源能多小就多小,请求距离能多短就多短,dom操作能不变就不变,能一次做的绝不两次做

性能优化是一个漫长的过程,慢慢体验⛽️

image.png

Rollup.js是什么?

作者:docoder@毛豆前端

Overview

  • Rollup 是前端模块化的一个打包工具,可以将小块代码编译成大块复杂的代码,例如 library 或应用程序。
  • Rollup 对代码模块可以使用新的标准化格式,如:ES6,而不是先前的解决方案,如:CommonJS 和 AMD。
  • Rollup 被广泛用于 Javascript libraries 的打包

Quick Start

For the browser

# compile to a <script> containing a self-executing function ('iife')
$ rollup main.js --flie bundle.js --format iife

For Node.js

# compile to a CommonJS module ('cjs')
$ rollup main.js --file bundle.js --format cjs

For browsers and Node.js

# UMD format requires a bundle name
$ rollup main.js --file bundle.js --format umd --name "myBundle"

For es module

$ rollup ./src/main.js --file ./dist/bundle.js --format es

Why Rollup

Tree-shaking

  • 也称为:live code inclusion

  • 使用 Rollup 处理代码模块, 采用 ES6 标准(使用 import/export),可以对模块文件进行静态分析,并可以排除任何未实际使用的代码
  • 为什么 ES Module 要优于 CommonJS
    • ES Module 是官方标准,有一个直接清晰的发展方向
    • CommonJS 只是在 ES Module 出现之前的一个特殊的暂时性的传统E标准
    • ES Module 可以对文件进行静态分析,进行 Tree-shaking 优化
    • ES Module 提供了更高级的特性,如,循环引用和动态绑定
    • Rollup ES6 modules Playgroup
// 使用 CommonJS,必须导入整个库

// Import a full utils object using CommonJS
var utils = require( 'utils' );
var query = 'Rollup';
// Use the ajax method of the utils object
utils.ajax( 'https://api.example.com?search=' + query ).then( handleResponse );

// 使用 ES6 module,无须导入整个库

// Import ajax function using the ES6 import statement
import { ajax } from 'utils';
var query = 'Rollup';
// Calling ajax function
ajax( 'https://api.example.com?search=' + query ).then( handleResponse );

Use Rollup in a CommonJS module

  • Rollup 坚定支持 ES module,CommonJS 没有在 Rollup kernel 中。
  • 需要使用插件 rollup-plugin-commonjs ,来将其转换为 ES Module,前提还需要安装和引入 rollup-plugin-node-resolve 插件,原因是 Rollup 不同于 Webpack 和 Browserify,它不知道如何处理模块中的依赖,所以 rollup-plugin-node-resolve 插件可以告诉 Rollup 如何查找外部模块。
  • 目前大部分的 npm 包都是以 commonjs 模块形式出现的,以防万一,还是需要安装和引入插件 rollup-plugin-commonjs 。另外,为了防止其他插件的改变破坏 commonjs 的检测,rollup-plugin-commonjs 应该用在其他插件转换模块之前。

Use it over Webpack ?

  • A number of open-source projects use it over Webpack

  • Webpack 获得了巨大成功,每月有百万级的下载,赋能了成千上万的网站和应用,有巨大的生态和资金支持,相比之下,Rollup 无足轻重

  • Facebook 采用 Rollup 来实现 React 的 build process,merge 了大量 pull request

  • Vue, Ember, Preact, D3, Three.js, Moment, and dozens of other well-known libraries also use Rollup

  • Rollup 以不同的目的被创建,Rollup 目的是要尽可能的高效的构建扁平的可分配的 Javascript libraries,充分使用 ES Module 的优点, 会将所有代码放在同一个位置统一进行验证,更快的生成更轻量级的代码。Rollup 不支持 code-splittingHMR,而且处理 CommonJS 时需要插件。

  • Webpack 支持 code-splitting , 实现了一个浏览器友好的 require ,将每个模块一个接一个的验证再打包。如果需要 on-demand loading,会很好;否则会造成性能浪费,尤其如果打包大量模块时,性能较差。

结论:

Use webpack for apps, and Rollup for libraries

  • 如果你需要 code-splitting,有很多 static assets,需要使用很多 CommonJS 依赖,使用 Webpack
  • 如果你的 codebase 是ES Module,写一些给其他人使用的代码或库,那么使用 Rollup

pkg.module

  • 未来,ES Module ( importexport) 会使统一标准,库的使用也会无缝。但现在很多浏览器以及 Node.js 不支持 importexport , 需要使用 UMD 或 CommonJS (Nodejs)
  • package.json 文件中增加 "module": "dist/my-library.es.js" , 可以同时更好的支持 UMD 和 ES Module
  • Webpack 和 Rollup 都会利用 pkg.module 来尽可能生成更高效的代码, 在某些情况下都会 tree-shaking

Example: Create a Typescript and React Module

ALL JS LIBRARIES SHOULD BE AUTHORED IN TYPESCRIPT

Install

$ yarn add typescript rollup rollup-plugin-typescript2 rollup-plugin-commonjs  rollup-plugin-peer-deps-external rollup-plugin-node-resolve --dev

$ yarn add react @types/react --dev
$ yarn add react-dom @types/react-dom --dev

tsconfig.json

{
  "compilerOptions": {
    "outDir": "build",
    "module": "esnext",
    "target": "es5",
    "lib": ["dom", "dom.iterable", "esnext"],
    "sourceMap": true,
    "allowJs": false,
    "jsx": "react",
    "declaration": true, // 自动生成 .d.ts
    "moduleResolution": "node",
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "resolveJsonModule": true,
    "downlevelIteration": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true
  },
  "include": ["src"], // 查找 ts 文件路径
  "exclude": ["node_modules", "build"] // 排出路径避免 build
}

rollup.config.js

import typescript from "rollup-plugin-typescript2";
import commonjs from "rollup-plugin-commonjs";
import external from "rollup-plugin-peer-deps-external";
import resolve from "rollup-plugin-node-resolve";

import pkg from "./package.json";

export default {
  input: "src/index.tsx",
  output: [
    {
      file: pkg.main,
      format: "cjs",
      // exports: "named",
      sourcemap: true
    },
    {
      file: pkg.module,
      format: "es",
      // exports: "named",
      sourcemap: true
    }
  ],
  external: [
    ...Object.keys(pkg.dependencies || {}),
    ...Object.keys(pkg.peerDependencies || {})
  ],
  plugins: [
    commonjs({  // 置于最前 ( 否则可能需要配置 namedExports 才能阻止保错 )
      include: ["node_modules/**"],
    }),
    external(),
    resolve(),
    typescript({
      rollupCommonJSResolveHack: true,
      exclude: "**/__tests__/**",
      clean: true
    })
  ]
};

package.json

{
  "name": "...",
  "version": "...",
  "description": "...",
  "author": "...",
  "main": "build/index.js",
  "module": "build/index.es.js", // pkg.module
  "jsnext:main": "build/index.es.js",
  "types": "build/index.d.ts",
  "license": "...",
  "repository": "...",
  "keywords": ['...', '...'],
  "scripts": {
    "build": "rollup -c",
    "start": "rollup -c -w",
    "prepare": "npm run build" // npm publish 时会先调用进行打包
  },
  "files": [ //  build 文件夹被打包进库里
    "build"
  ],
  "peerDependencies": {
    "react": ">=16.8.0",
    "react-dom": ">=16.8.0"
  },
  "dependencies": {
    ...
  },
  "devDependencies": {
    "@types/react": "^16.8.23",
    "@types/react-dom": "^16.8.4",
    "react": "^16.8.6",
    "react-dom": "^16.8.6",
    "rollup": "^1.16.6",
    "rollup-plugin-commonjs": "^10.0.1",
    "rollup-plugin-node-resolve": "^5.2.0",
    "rollup-plugin-peer-deps-external": "^2.2.0",
    "rollup-plugin-typescript2": "^0.21.2",
    "typescript": "^3.5.2"
  }
}

Developing locally

# 在开发的库文件目录下
$ yarn link # 会在全局生成一个 link 文件 (如:.nvm/versions/node/v10.15.3/lib/node_modules/your-package-name),link 到此库文件目录

# 在要使用库的 example project 文件目录下
$ yarn link your-package-name # link 到全局的 link 文件,从而又 link 到开发的库文件目录

# Fix Duplicate React: https://github.com/facebook/react/issues/15315#issuecomment-479802153
$ npm link ../example/node_modules/react

跨域开发的几种解决方案

作者:颜亦浠@毛豆前端

本地开发过程中,最常遇到的就是出现跨域,无法请求的问题。如何解决在开发中遇到的跨域问题,今天整理了4种解决方法,供大家参考。

一、何为跨域

跨域出于浏览器的同源策略的限制,浏览器本身会限制跨域请求(严格来说,只是限制跨域的读操作)。那么何为跨域呢?非同源请求均为跨域,即:如果两个请求的协议、域名、端口号只要有一个不同,即为非同源即为跨域。

二、常见解决跨域方案

一般多用于本地自测或者前后端开发部署均为分离的情况

1、webpack的proxyTable方案

在一般项目中都会有webpack对应的开发环境的配置文件:webpack.dev.js,在配置项中加入ProxyTable的配置项即可:

proxy: {
          '/api': {
          changeOrigin: true,
          target: 'http://******.*****.com',
    }
}

如果前后端前缀不匹配或者后端前缀不统一的情况,可以增加pathRewrite属性来统一:

proxy: {
            '/EntryApp': {
              changeOrigin: true,
              target: 'http://******.*******.com',
              pathRewrite: {"^/EntryApp": "/EntryApp"}
        },
    }

proxyTable实现跨域可能存在的问题:

1.cookie丢失,接口无法访问

2.post请求报403错

2、Switchhosts工具

SwitchHosts是一个管理、快速切换Hosts小工具,软件开源,可以实现一键切换Hosts配置。SwitchHosts需要管理本机的ip和端口的映射,所以需要以管理员身份运行。

打开SwitchHosts之后,需要在Myhost当中配置对应的映射,当左手边处于打开状态的时候,文件是处于只读的状态,需要编辑的话,就需要让文件处于关闭状态,不同环境还可以分文件配置,直接照下图配置即可:

SwitchHosts使用中可能会遇到的问题:

1.端口号默认为80,如果不是,需要配置上对应的端口号

2.浏览器会有先考虑代理工具的代理。

3、Uuaper

uuaper是百度提供的一款基于nodejs,用于解决前端跨域问题的工具。具体的安装与配置可以去npm官网查找Uuaper,使用中需要结合nodejs,并需要具有自动认证功能:

var express = require('express');
var app = express();
 
var uuaper = require('uuaper');
    app.use('/api', new uuaper({
        target: 'http://xxx.xxx.com/',
        debug: true,
        auth: {
        server: 'http://xxx.baidu.com/login?',
        username: 'xxx',
        password: 'xxx',
    }
}));

4、Nginx

Nginx是一个免费的,开源的高性能的HTTP和反向代理服务器。

通常,线上前后端分开部署时,用nginx比较多。

nginx.conf是主配置文件,有若干个部分组成,每一部分都用{}区分。主要包括:

  • main:nginx的全局配置,对全局生效
  • events:影响nginx服务器或与用户的网络连接
  • http:可以嵌套多个server,配置代理,缓存,日志等
  • server:配置虚拟主机的相关参数,一个http可以有多个server
  • nginx解决跨域的基本方法是在sever中配置proxy_pass:
    // 前端服务的域名为 fe.**.com
    // 后端服务的域名为 dev.**.com
    server {
    listen: 80,
    server_name: fe.**.com,
    location / {
         proxy_pass dev.**.com
    }
    }
    

    根据实际需求,还可以添加一些其他的指令,比如:

  • proxy_connect_timeout:nginx从接受请求至连接到上游服务器的最长等待时间
  • proxy_cookie_domain:替代上游服务器的set_cookie头的domain属性
  • proxy_cookie_path:替代上游服务器的set_cookie头的path
  • proxy_set_header:重写发送到上游服务器头的内容,也可以通过将某个头部的值设置为空字符串,而不发送某个头部的方式发放实现

Vue页面转Pdf实践

作者:颜亦浠@毛豆前端

这一次我们来聊聊如何把页面转换成Pdf文件,经常会有这种场景,一些合同、协议等的页面需要进行下载,而且需要和页面保持一致,那么最好的方式就是直接把页面转换成相应的格式就好了,目前基本上就是Doc和Pdf这2种比较流行,我们就以Vue写的页面为例来看看如何转成Pdf文件。

模块依赖

主要依赖两个模块:html2canvas和jspdf:

(1)npm install –save html2canvas(将页面html转换成图片)

(2)npm install –save jspdf(将图片生成pdf)

定义HtmlToPdf插件

创建一个htmlToPdf.js文件在指定位置内容如下:

// 导出页面为PDF格式

import html2canvas from 'html2canvas'

import JSPDF from 'jspdf'

export default {

    install(Vue, options) {
        Vue.prototype.ExportSavePdf = function (htmlTitle, currentTime) {
            var element = document.getElementById('pdfContent')
            html2canvas(element, {

                logging: false

            }).then(function (canvas) {
                var pdf = new JSPDF('p', 'mm', 'a4') // A4纸,纵向

                var ctx = canvas.getContext('2d')

                var a4w = 170;
                var a4h = 257 // A4大小,210mm x 297mm,四边各保留20mm的边距,显示区域170x257

                var imgHeight = Math.floor(a4h * canvas.width / a4w) // 按A4显示比例换算一页图像的像素高度

                var renderedHeight = 0

                while (renderedHeight < canvas.height) {
                    var page = document.createElement('canvas')

                    page.width = canvas.width

                    page.height = Math.min(imgHeight, canvas.height - renderedHeight) // 可能内容不足一页

                    // 用getImageData剪裁指定区域,并画到前面创建的canvas对象中
                    page.getContext('2d').putImageData(ctx.getImageData(0, renderedHeight, canvas.width, Math.min(imgHeight, canvas.height - renderedHeight)), 0, 0)
                    pdf.addImage(page.toDataURL('image/jpeg', 1.0), 'JPEG', 10, 10, a4w, Math.min(a4h, a4w * page.height / page.width)) // 添加图像到页面,保留10mm边距
                    renderedHeight += imgHeight

                    if (renderedHeight < canvas.height) {
                        pdf.addPage()
                    } // 如果后面还有内容,添加一个空页
                    // delete page;
                }
                pdf.save(htmlTitle + currentTime)
            })
        }
    }
}

安装插件

接下来我们需要把上面实现的插件安装到Vue中: 在main.js中安装插件

import htmlToPdf from '@/components/utils/htmlToPdf'
Vue.use(htmlToPdf)

使用ExportSavePdf转换

定义好要转换的dom容器,然后直接调用ExportSavePdf函数,传入参数即可:

<template>
  <div>
    <div ref="pdfContent" id="pdfContent">
      <el-table :data="tableData" border style="width: 100%">
        <el-table-column fixed prop="date" label="日期" width="150"></el-table-column>
        <el-table-column prop="name" label="姓名" width="120"></el-table-column>
        <el-table-column prop="province" label="省份" width="120"></el-table-column>
        <el-table-column prop="city" label="市区" width="120"></el-table-column>
        <el-table-column prop="address" label="地址" width="300"></el-table-column>
        <el-table-column prop="zip" label="邮编" width="120"></el-table-column>
        <el-table-column fixed="right" label="操作" width="100">
          <template slot-scope="scope">
            <el-button @click="handleClick(scope.row)" type="text" size="small">查看</el-button>
            <el-button type="text" size="small">编辑</el-button>
          </template>
        </el-table-column>
      </el-table>
    </div>
    <el-button type="danger" @click="ExportSavePdf()">导出PDF</el-button>
  </div>
</template>

<script>
import html2canvas from 'html2canvas'
import JSPDF from 'jspdf'
export default {
  methods: {
    handleClick(row) {
      console.log(row)
    },
  },

  data() {
    return {
      htmlTitle: 'PDF名称',
      nowTime: '',
      tableData: [
        {
          date: '2016-05-03',
          name: '王小虎',
          province: '上海',
          city: '普陀区',
          address: '上海市普陀区金沙江路 1518 弄',
          zip: 200333,
        },
        {
          date: '2016-05-02',
          name: '王小虎',
          province: '上海',
          city: '普陀区',
          address: '上海市普陀区金沙江路 1518 弄',
          zip: 200333,
        },
        {
          date: '2016-05-04',
          name: '王小虎',
          province: '上海',
          city: '普陀区',
          address: '上海市普陀区金沙江路 1518 弄',
          zip: 200333,
        },
        {
          date: '2016-05-01',
          name: '王小虎',
          province: '上海',
          city: '普陀区',
          address: '上海市普陀区金沙江路 1518 弄',
          zip: 200333,
        },
      ],
    }
  },
}
</script>

JavaScript变量与函数提升

作者:小斯基@毛豆前端

本文要点:

  • 函数提升;
  • var,let,const三种方式变量提升的区别;

首先看一些大家熟悉的代码:
code1:

// foo调用在定义之前
foo();
function foo() {
  console.log('hello world');
}
// hello world

code2:

// 报错
foo();
var foo = function() {
  console.log('hello world');
}
// TypeError: foo is not a function

code3:

// 不报错
console.log(`a is ${a}`);
var a = 'hello world'
// a is undefined

code4:

// 报错
console.log(`a is ${a}`);
let a = 'hello world'
// ReferenceError: a is not defined

code5:

// 报错
console.log(`a is ${a}`);
const a = 'hello world'
// ReferenceError: a is not defined

下面进入正题: JS引擎会在正式执行之前先进行一些预处理,在这个过程中,首先将变量定义和函数定义的各个阶段操作(创建、初始化和赋值)提升至当前作用域的顶端,然后进行接下来的处理。(注:由于引擎的不同,预处理也会有所差异)。

code1和code2比较好理解。code1中函数定义会被提升到当前作用域顶部,然后再做后续执行,即foo函数的定义被js引擎自动提升到foo()前面,所以可以正常调用。而code2中,foo实际上是一个变量定义,然后将foo变量指向一个匿名函数, 那既然变量定义也会有提升,为何会报错呢?

code3的变量提升理解起来也比较简单,但是code4,code5中分别使用let、const抛出了错误,看来js对var、let、const三种定义方式的变量提升有所区别。下面将详细讲解:

js变量和函数定义有三个阶段: 创建create、初始化initialize 和赋值assign,针对不同的定义对不同的阶段做提升:

  1. 函数定义的创建、初始化和赋值三个阶段都被提升了;
  2. var定义的创建、初始化被提升了,赋值未被提升;
  3. let的创建被提升了,但初始化和赋值都未被提升;
  4. const的创建被提升了,初始化未被提升,特殊的一点是const没有赋值阶段,所以const定义的变量值不能改变;

现在我们详细梳理下上述代码:

  1. code1中函数定义的三个阶段都被提升,所以是先创建变量foo(此时还无法使用foo),然后将foo初始化为undefined,再将foo赋值为函数体,最后执行foo();
  2. code2中是var变量定义,先创建foo,然后将其赋值为undefined,之后调用foo(),所以这里会报错;
  3. code3和code2类似,foo在初始化后执行console.log,所以打印出undefined;
  4. code4中是let变量定义,先创建foo,由于初始化未被提升,所以创建之后立即执行console.log,于是就报错了;
  5. code5与code4报错过程类似。

现在我们理解了js对各种变量和函数定义做提升时的区别。那么变量和函数定义的提升有没有优先级顺序呢?答案是有的。请看如下代码:

console.log(foo);
function foo() {
  console.log('hello world');
}

var foo = 1;
console.log(foo);

// [Function: foo]
// 1

这段代码可解释为: 首先找到js引擎先找到var定义语句,创建变量foo,然后将其初始化为undefined,之后找到function定义语句,重新创建foo,之后将其赋值为函数体(因为function的赋值过程也被提升了),然后执行第一个console.log,接下来 才执行foo变量的赋值过程(因为var定义的赋值过程未被提升),最后执行第二个console.log。若将代码中var换成let,将会抛出SyntaxError: Identifier ‘foo’ has already been declared。因为let变量定义不允许有第二次 创建过程, function定义的创建过程就抛错了。

关于let还有一个很有意思的现象,假如我们执行如下代码:

let x = x; // x之前未定义过
// Uncaught ReferenceError: Cannot access 'x' before initialization

x = 1;
// Uncaught ReferenceError: x is not defined

在抛出一段错误之后会发现,在当前上下文中不能再使用名为x的变量了,也不能对x赋值。导致这个现象的原因大概是: let定义首先创建x,之后执行代码,发现let x = x等号右边的x对x产生了引用,由于变量初始化之前就被引用了,所以抛出Cannot access ‘x’ before initialization,并且这个错误会导致let x语句在创建x后初始化失败(前面的 错误导致不能进入初始化过程),由于js变量定义的初始化过程只有一次,一旦失败就不能再次初始化了,此时的x处于一个已创建但又不能初始化的状态,即所谓的暂时性死区, 此后对x的引用或赋值都会抛出错误。

另外,ES6中的class声明也存在提升,但是它和let、const一样,有一些限制,如果在声明位置之前引用,会抛出一个异常。

最后提醒大家在写js代码过程中,尽量遵循一点: 先声明,后使用

程序人生:如何提效,如何管理时间

作者:胡籽@毛豆前端

平时常听到 “天呐,这周就这么过去了,我啥都没干” “今天我啥都没做” 这种焦虑时间过得快、没时间学习的话语,👇分享一些个人的见解

花时间补基础,读文档

作为程序猿,debug是我们的日常操作,debug 即费时间又费脑力精神力,但是你是否发现很多问题归根到底是因为基础不扎实或者文档没有看透。

基础是技术的支撑,花时间打好基础而不是一味追各种新技术。一旦基础扎实,学习各种新技术分分钟搞定,因为新的技术,究其根本都是相通的。这就好比你要吃饭的时候要先学会拿筷子勺子,而不是捧着碗一个劲的往嘴里塞。。。(这比喻有点粗暴~~😊)

文档同样是程序猿的技术基础。优秀的库,开发人员肯定已经把如何使用这个库都写在文档中了,仔细阅读文档一定会是少写 bug 的最省事路子。

画个图,想想再做

不知各位程序猿小哥哥小姐姐是否遇到过这样的问题,需求一下来,看一眼,然后撸起袖子马上按照设计稿开始 coding , 然而进行到半途的时候出现了某个问题导致返工。

如果你存在这样的问题,我很推荐在看到设计稿和需求的时候花点时间想一想,画一画。考虑一下设计稿中是否可以找到可以拆分出来的复用组件,是否存在之前写过的组件。该如何组织这个界面,数据的流转是怎么样的。然后画一下这个页面的需求,最后再动手做。

我之前特意针对这点做了一个试验: 在某个需求中做了个规划:规定自己每天下午5点之后的时间用来学习。 如何在既要完成业务需求的同时,又能达到自己制定的规划呢? 这就要求在写需求之前先想好:界面如何组织,数据的流转,功能的实现。这些都想好之后,那么接下来写代码的时间就能缩短一半,当然这同时要求在每天5点之前的这段时间精力是相对集中的。(时间是可以挤出来的,关键看方法)在做每件事情的时候我们要想:为什么要这么做?为什么是这样的?这才是关键的

列好Todo

一般在周末规划好下个工作周要做的事情,并将事情拆分成小点。给ToDo排好优先级,高优先级的最先处理。同优先级的优先做简单的,这样可以给自己提高成就感。

反思

每周结束对自己上周工作进行一遍反思,你会发现某些事情可以处理得更完美,争取在下一次去改进。比如你给大家分享了一篇文章,过后反思会发现可以做得更好。

高效工作的必备技能(我认为的)

  • 制作任务清单,将任务分类

    为了保证工作的正常与高效完成,可以去将工作拆分成多个任务去进行处理,然后多个任务在进行分类,区分任务的优先级以及种类,列 TodoList

  • 集中时间批量处理任务

    对于一些任务我们需要在特定的时间去集中处理它,这样做的目的是提高时间的利用率。如果在不同的时间去处理零散任务,这会导致一些有牵连性的任务由于没有集中去处理,从而在完成单个未完成的任务时还需花费时间去联系之前的任务,这不仅浪费时间,效率还低,整体计划甚至有可能会被打乱。

  • 保证良好的作息时间(保证睡眠时间充足)

    这里的作息时间指的是循环式的作息时间。通常我们采用的是线式的作息时间,这个线性的计划是平均安排时间来执行工作。而循环式的作息时间是花费很少的一部分时间完成大部分的工作。优点在于能够让计划安排变得张弛有度,靠别死气沉沉。比如: 1、 周末利用半天时间输出自己下周计划,这样避免新的工作周到来时还处于迷茫状态。剩余时间去除工作,放松自己(防止自己精疲力尽😊) 2、 晚上尽可能的不干活不加班,将工作安排到白天完成,晚上时间尽可能的用来学习、看书或放松自我。我并不赞成以通宵加班到深夜的方式来完成工作,然后第二天早上没有精神工作或者起不来,这样会使自己陷入一个恶性循环,大大降低效率。 3、 每天给自己设定30分钟的学习任务。相信这会让你学习的注意力更集中

  • 锻炼身体,让身体机能充沛

    常说 身体健康 是革命的本钱,只有健康的身体才能够持续保持有精力去学习,身体健康是学习的最基本条件。

《如何高效学习》 以上是我读这篇文章之后的一些自我实践,学会如何更好的管理时间。他人的方法转化为自己方法的一些总结。

ES6之Promise再解读

作者:小火柴@毛豆前端

ES6中的Promise对象是异步编程的一个重要的点,下面是我整理的学习笔记跟大家分享一下,在这之前我觉得有必要先了解一下JS事件机制和一些相关的异步操作。

JS事件机制

在说JS事件机制之前咱们先提一嘴浏览器进程。 浏览器是多进程运行的,JS引擎只是浏览器渲染进程中的一个单线程,在单线程中一次只能执行一个任务,多任务处理的情况下就要进行排队等候顺序执行,因此会出现若某任务执行很耗时,等待时间过长而卡死的情况,为了解决由于单线程特性出现的”卡死”问题,就用到了咱们今天要说的JS异步。 下图是浏览器进程思维导图: 1.png-581.9kB

JS引擎遇到一个异步事件后并不会一直等待其返回结果,而是将这个事件挂起,继续执行执行栈中的其他任务。当一个异步事件执行完毕并返回结果后,JS会将这个事件加入与当前执行栈不同的另一个队列–事件队列。被放入事件队列不会立即执行其回调,而是等待当前执行栈的所有任务执行完毕,主线程处于闲置状态时,主线程回去查找事件队列是否有任务。如果有,那么主线程会取出排在第一位的事件,并把该事件对应的回调放到执行栈中,然后执行其中的同步代码…,如此反复,这就形成了一个无线循环,也就是我们说的事件循环(Event Loop) 下图就是JS事件机制说明: image.png-83.4kB

宏任务 & 微任务

事件循环过程是一个宏观的表述,由于异步任务之间并不相同,其执行优先级也有区别。因此不同的异步任务被分为宏任务和微任务两类。 image.png-41kB

运行机制: 1.执行一个宏任务(栈中没有就从事件队列中获取) 2.执行过程中如果遇到微任务,就将它添加到微任务的任务队列中 3.宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行) 4.当前宏任务执行完毕,开始检查渲染,然后GUI线程接管渲染 5.渲染完毕后,JS线程继续接管,开始下一个宏任务(从事件队列中获取)

经典面试题:

setTimeout(() => { console.log(4); }, 0);
new Promise(resolve => {
    console.log(1);
    resolve()
    console.log(2)
}).then(() => {
    console.log(5)
})
console.log(3)
// 结果:1 2 3 5 4

Promise其实就是一个异步的微任务,那我们就开启Promise之旅吧。

Promise

简介

Promise 是异步编程的一种解决方案,比传统的解决方案 (回调函数和事件)更合理和更强大。简单说它就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上,Promise 是一个对象,从它可以获取异步操作的消息。Promise操作后返回的对象还是一个新的Promise对象,所以支持链式调用,它可以把异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数,更便于理解与阅读。

Promise主要有以下特点

  1. Promise对象有不受外界影响的三个状态:

pending(进行中) fulfilled(已成功) rejected(已失败) 只有异步操作的结果才能确定当前处于哪种状态,任何其他操作都不能改变这个状态。这也是Promise(承诺)的由来。

  1. Promise状态一旦改变就不会再变,任何时候都可以得到这个结果。它的状态改变只有两种结果:

pending —-> fulfilled pending —-> rejected 只要有其中一种情况发生,状态就凝固了,不会再变,会一直得到这个结果,后续再添加Promise的回调函数也只能拿到前面状态凝固的结果

Promise缺点:

1.无法取消Promise,一旦新建它就会立即执行,无法中途取消

2.如果不设置回调函数,Promise内部抛出的错误,不会反应到外部

3.当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)

Promise API

从图中也可以看出来,Promise既是一个对象也是一个构造函数 image.png-35.5kB

Promise基本用法

resolve / reject

let promise = new Promise((resolve, reject) => {
    if(/**操作成功 */){
        resolve(seccess)
    }else{
        reject(error)
    }
})

Promise接收一个函数作为参数,函数里有resolve和reject两个参数,这两个参数其实是Promise内置的两个方法,会在异步操作执行结束后调用,可以将异步操作的结果回传至回调函数,以确定Promise最终的一个状态(是fulfilled还是rejected)。 resolve方法的作用是将Promise的pending状态变为fulfilled,在异步操作成功之后调用,可以将异步返回的结果作为参数传递出去。 resolve还可以接受Promise实例作为参数

let p1 = new Promise((resolve, reject) => {
    reject('error')
})
let p2 = new Promise((resolve, reject) => {
    resolve(p1)
})
p2.then(s => console.log(s))
    .catch(e => console.log(e))

reject方法的作用是将Promise的pending状态变为rejected,在异步操作失败之后调用,可以将异步返回的结果作为参数传递出去。 ⚠️他们之间只能有一个被执行,不会同时被执行,因为Promise只能保持一种状态。

then()

Promise实例确定后,可以用then方法分别指定fulfilled状态和rejected状态的回调函数。

let promise = new Pormise();
promise.then(success => {
    // 等同于上面的resolve(success)
}, error => {
    // 注意:此处无法捕获onfulfilled抛出的错误
})

then(onfulfilled,onrejected)方法中有两个参数,两个参数都是函数,第一个参数执行的是resolve()方法(即异步成功后的回调方法),第二参数执行的是reject()方法(即异步失败后的回调方法)(第二个参数可选)。它返回的是一个新的Promise对象,因此可以采用链式写法(解决了异步串行的操作,避免了传统异步串行操作层层嵌套的问题)。

function createPromise(p, state){
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if(state === 0){
                reject(`error, ${p}`)
            }else{
                resolve(`success, ${p}`)
            }
        }, 0)
    })
}
createPromise('p1', 1).then(success => {
    console.log('111', success)
    return createPromise('p2', 2)
}).then(success => {
    console.log('222', success)
    return createPromise('p3', 3)
}).then(success => {
    console.log('333', success)
})

// 111 success, p1
// 222 success, p2
// 333 success, p3

⚠️then 方法注意点:简便的 Promise 链式编程最好保持扁平化,不要嵌套 Promise。

catch()

catch方法实际上是.then(null,onrejected)的别名,用于指定发生错误时的回调函数。作用和then中的onrejected一样,它还可以捕获onfulfilled抛出的错,弥补了then的第二个回调onrejected的缺陷

new Promise().then(success => {
    // ...
}).catch(error => {
    // ...
})

⚠️注意:串行操作时只能捕获前面Promise抛出的错,而无法捕获在他们后面的Promise抛出的错

createPromise('p1', 0).then(success => {
    console.log('111', success)
    return createPromise('p2', 0)
}).then(success => {
    console.log('222', success)
    return createPromise('p3', 0)
}).catch(error => {
    console.log('333', error)
})
// 333 error, p1

finally()

finally方法用于指定不管Promise对象最后状态如何,都会执行的操作。 该方法不接受任何参数,所以跟Promise的状态无关,不依赖于Promise的执行结果

createPromise('p1', 1).then(success => {
    console.log('111', success)
}).catch(error => {
    console.log('222', error)
}).finally(() => {
    console.log('finally')
})
// 111 success, p1
// finally

all()

Promise.all方法接受一个以Promise实例组成的数组作为参数。Promise的all方法提供了并行执行异步操作的能力,并且在所有异步操作都执行完毕后才执行回调,只要其中一个异步操作返回的状态为rejected那么Promise.all()返回的Promise即为rejected状态,此时第一个被reject的实例的返回值,会传递给Promise.all的回调函数:

Promise
.all([createPromise('p1', 1), createPromise('p2', 1)])
.then(r => { console.log(r) })
// ["success, p1", "success, p2"]
Promise
.all([createPromise('p1', 1), createPromise('p2', 0)])
.then(r => { console.log(r) })
.catch(e => { console.log(e) })
// error, p2

若Promise.all的Promise实例参数自己定义了catch方法且被rejected,就不会触发Promise.all()的catch方法了,而是执行了then

let p2 = createPromise('p2', 0).catch(e => {
    console.log('p2-catch', e)
})
Promise
    .all([createPromise('p1', 1), p2])
    .then(r => { console.log(r) })
    .catch(e => { console.log(e) })
    // p2-catch error, p2
    // ["success, p1", undefined]

race()

Promise的race方法和all方法类似,区别在于all方法的效果实际上是(谁慢以谁为准),而race方法则是(谁快以谁为准)

Promise
.race([createPromise('p1', 1), createPromise('p2', 0)])
.then(r => { console.log(r) })
.catch(e => { console.log(e) })
// success, p1

移动端1px解决方案

作者:empty@毛豆前端

前言

移动端web项目越来越多,设计师对于UI的要求也越来越高,比如1px 的边框。在高清屏下,移动端的1px 会很粗。 比如,这个是假的1像素 这个是真的1像素

一、产生原因

那么为什么会产生这个问题呢?主要是跟一个东西有关,DPR(devicePixelRatio) 设备像素比,它是默认缩放为100%的情况下,设备像素和CSS像素的比值。

window.devicePixelRatio=物理像素 /CSS像素

目前主流的屏幕DPR=2 (iPhone 8),或者3 (iPhone 8 Plus)。拿2倍屏来说,设备的物理像素要实现1像素,而DPR=2,所以css 像素只能是 0.5。一般设计稿是按照750来设计的,它上面的1px是以750来参照的,而我们写css样式是以设备375为参照的,所以我们应该写的0.5px就好了啊! 试过了就知道,iOS 8+系统支持,安卓系统不支持。

二、解决方案

1、WWDC对iOS统给出的方案

推荐指数:**

在 WWDC大会上,给出来了1px方案,当写 0.5px的时候,就会显示一个物理像素宽度的 border,而不是一个css像素的 border。 所以在iOS下,你可以这样写。

border:0.5px solid #E5E5E5

可能你会问为什么在3倍屏下,不是0.3333px 这样的?经过我测试,在Chrome上模拟iPhone 8Plus,发现小于0.46px的时候是显示不出来。 总结:

  • 优点:简单,没有副作用
  • 缺点:支持iOS 8+,不支持安卓。后期安卓follow就好了。

    2、使用边框图片

    推荐指数:**

      border: 1px solid transparent;
      border-image: url('./../../image/96.jpg') 2 repeat;
    

    图片自己随便截图的,建议自己做一张图片

图片的颜色就是此后border的颜色

这个方法在W3CPlus 上的例子讲的非常细致 https://www.w3cplus.com/content/css3-border-image总结:

代码是怎样实现的呢?

box-shadow: 0  -1px 1px -1px #e5e5e5,   //上边线
            1px  0  1px -1px #e5e5e5,   //右边线
            0  1px  1px -1px #e5e5e5,   //下边线
            -1px 0  1px -1px #e5e5e5;   //左边线

前面两个值 x,y 主要控制显示哪条边,后面两值控制的是阴影半径、扩展半径。 其实方法可以到这个地址线上尝试一下

总结

  • 优点:使用简单,圆角也可以实现
  • 缺点:模拟的实现方法,仔细看谁看不出来这是阴影不是边框。

    4、使用伪元素

    推荐指数:****

    这个方法是我使用最多的,做出来的效果也是非常棒的,直接上代码。

    1 条border

    .setOnePx{
      position: relative;
      &::after{
        position: absolute;
        content: '';
        background-color: #e5e5e5;
        display: block;
        width: 100%;
        height: 1px; /*no*/
        transform: scale(1, 0.5);
        top: 0;
        left: 0;
      }
    }
    

    可以看到,将伪元素设置绝对定位,并且和父元素的左上角对齐,将width 设置100%,height设置为1px,然后进行在Y方向缩小0.5倍

    4 条border

    .setBorderAll{
         position: relative;
           &:after{
               content:" ";
               position:absolute;
               top: 0;
               left: 0;
               width: 200%;
               height: 200%;
               transform: scale(0.5);
               transform-origin: left top;
               box-sizing: border-box;
               border: 1px solid #E5E5E5;
               border-radius: 4px;
          }
        }
    

    同样为伪元素设置绝对定位,并且和父元素左上角对其。将伪元素的长和宽先放大2倍,然后再设置一个边框,以左上角为中心,缩放到原来的0.5倍 总结:

  • 优点:全机型兼容,实现了真正的1px,而且可以圆角。
  • 缺点:暂用了after 伪元素,可能影响清除浮动。

    5、设置viewport的scale值

    推荐指数:*****

    这个解决方案是利用viewport+rem+js 实现的。 参考了网上的一个例子 移动端1像素边框问题的解决方案,自己手动实现了一下。 效果不错。 上代码

    <html>
      <head>
          <title>1px question</title>
          <meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
          <meta name="viewport" id="WebViewport" content="initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no">        
          <style>
              html {
                  font-size: 1px;
              }            
              * {
                  padding: 0;
                  margin: 0;
              }
              .top_b {
                  border-bottom: 1px solid #E5E5E5;
              }
    
              .a,.b {
                          box-sizing: border-box;
                  margin-top: 1rem;
                  padding: 1rem;                
                  font-size: 1.4rem;
              }
    
              .a {
                  width: 100%;
              }
    
              .b {
                  background: #f5f5f5;
                  width: 100%;
              }
          </style>
          <script>
              var viewport = document.querySelector("meta[name=viewport]");
              //下面是根据设备像素设置viewport
              if (window.devicePixelRatio == 1) {
                  viewport.setAttribute('content', 'width=device-width,initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no');
              }
              if (window.devicePixelRatio == 2) {
                  viewport.setAttribute('content', 'width=device-width,initial-scale=0.5, maximum-scale=0.5, minimum-scale=0.5, user-scalable=no');
              }
              if (window.devicePixelRatio == 3) {
                  viewport.setAttribute('content', 'width=device-width,initial-scale=0.3333333333333333, maximum-scale=0.3333333333333333, minimum-scale=0.3333333333333333, user-scalable=no');
              }
              var docEl = document.documentElement;
              var fontsize = 32* (docEl.clientWidth / 750) + 'px';
              docEl.style.fontSize = fontsize;
          </script>
      </head>
      <body>
          <div class="top_b a">下面的底边宽度是虚拟1像素的</div>
          <div class="b">上面的边框宽度是虚拟1像素的</div>
      </body>
    </html>
    

    总结

  • 优点:全机型兼容,直接写1px不能再方便
  • 缺点:适用于新的项目,老项目可能改动大

    三、踩过的坑

    一部血泪史。。。。征服不了UI,只能征服自己。

    1、使用伪元素方法,伪类里面再设置伪元素,可以选择到吗?

    看图,需要改中间的竖线 上代码

        &:nth-child(2){
          //border-color: #e5e5e5 !important;
            border: 0;
          position: relative;
          &:after{
            position: absolute;
            content: '';
            background-color: #e5e5e5;
            display: block;
            width: 100%;
            height: 1px; /*no*/
            transform: scale(1, 0.5);
            top: 0;
          }
        }
    

    然而上面代码展示出来的样式是这样的 为什么中间的竖线没有了?!最初我以为在伪类下面,再写伪元素after,可能会拿不到。 看到这里发现,是有after伪元素的,但是好像位置不对,跑到上面去了。我是想要竖线的,到底什么原因呢?最后在安哥的指导找了一种方法解决了这个问题,才明白其中的真相。

    原来是我的width 和 height 设置的有问题,对于竖线,应该是width =1px,height=100%,然后再缩放 X 方向0.5倍,这样竖线就出来了;同样,设置水平线,应该是width=100%,height=1px,然后再缩放Y方向0.5倍

知道了原因,再也不担心写错了。

&:nth-child(2){
        position: relative;
        &:after{
          position: absolute;
          content: '';
          top: 0;
          left: 0;
          width: 1px;
          height: 100%;
          transform: scaleX(0.5);
          background: #e5e5e5;
          transform-origin: 0 0;
        }
      }

这样是可以的,

 &:nth-child(2){
        position: relative;
        &:after{
          content:" ";
          position:absolute;
          top: 0;
          left: 0;
          width: 200%;
          height: 200%;
          transform: scale(0.5);
          transform-origin: left top;
          box-sizing: border-box;
          border-left: 1px solid #E5E5E5;
        }
      }

这样也是可以的。

2、为什么还是拿不到伪元素

我以为知道了上面的方法就可以快快乐乐的写1像素 border 了,然而马上又怀疑自己了。 上面这个输入框的1像素,需要的所有的border都是1px,我使用了宽高放大200%后再缩小0.5倍的方法。 这就奇怪了,同样的方法在别的地方都有效的,为什么在这里不显示了。找了好久,最后找到这篇文章使用 CSS 伪元素需要注意的 ,然后发现: 所以不显示的原因找到了,==输入框Textarea不支持伪元素==、 没办法了,伪元素的方法不能用,只能使用其他的方法了。

四、总结

总结一下,新项目最好使用的是设置viewport的scale值,这个方法兼容性好,后期写起来方便。老项目的话,改起来可能比较多,用的比较多的方法就是伪元素+transform的方法。 其他的背景图片,阴影的方法毕竟还是不太灵活,而且兼容性不好。

HTTP/2轻松入门

作者:葉@毛豆前端

一、定义

模板方法模式定义了一个算法的步骤,并允许子类别为一个或多个步骤提供其实践方式。让子类别在不改变算法架构的情况下,重新定义算法中的某些步骤

以上的定义可以知道模板方法模式由两部分组成

  1. 抽象的实现算法(抽象类)
  2. 子类的具体实现方法(实现类)

模板方式将是共性的部分放在父类中,不同的部分放在子类中依据不同的情况分别实现。这样的实现方式可以避免重复的行为在各个子类中冗余

二、例子

《Head First设计模式》中讲到coffee or tea这个例子是个经典的模板方法模式,作为一个资深吃货,由于总吃外卖,且在北方生活基本吃干饭,一个南方人,总是对于吃饭喝汤的执着甚深,也愿意自己动手煲汤,发现炖汤做菜也是些符合模板方法模式->_->!! 果然吃货的脑回路就是不一样,哈哈。接下来看下我煲玉米排骨汤和牛肉萝卜汤的的例子:

玉米排骨汤

通常炖玉米排骨汤的步骤是这样的:

  1. 用凉水把排骨焯一遍
  2. 捞出放炖盅,加上生姜、料酒、玉米
  3. 煮一小时
  4. 排骨玉米汤盛碗里

我们用代码大致模拟下这个过程:

let Yumipaigu = function(){}
Yumipaigu.prototype.chaopaigu = function() {
    console.log('排骨焯水');
}
Yumipaigu.prototype.addYumi = function() {
    console.log('加生姜、料酒、玉米');
}
Yumipaigu.prototype.boil = function(){
    console.log('煮一小时');
}
Yumipaigu.prototype.pourInBowl = function() {
    console.log('排骨玉米汤盛碗里');
}
Yumipaigu.prototype.init = function() {
    this.chaopaigu();
    this.addYumi();
    this.boil();
    this.pourInBowl();
}
let yumipaigu =  new Yumipaigu();
yumipaigu.init();

至此,一份排骨玉米汤就完成啦!我们看下炖牛肉萝卜汤是怎么样的一个过程:

牛肉萝卜汤

  1. 用凉水把牛肉焯一遍
  2. 捞出放炖盅,加入葱姜蒜、桂皮、香叶、花椒、萝卜
  3. 煮四十分钟
  4. 牛肉萝卜汤盛碗里

我们仍然用代码大致表示下:

let Niurouluobo = function(){}
Niurouluobo.prototype.chaoniurou = function() {
    console.log('牛肉焯水');
}
Niurouluobo.prototype.addNiurou = function() {
    console.log('加入葱姜蒜、桂皮、香叶、花椒、萝卜');
}
Niurouluobo.prototype.boil = function(){
    console.log('煮四十分钟');
}
Niurouluobo.prototype.pourInBowl = function() {
    console.log('牛肉萝卜汤盛碗里');
}
Niurouluobo.prototype.init = function() {
    this.chaoniurou();
    this.addNiurou();
    this.boil();
    this.pourInBowl();
}
let niurouluobo =  new Niurouluobo();
niurouluobo.init();

现在我们把两个的过程都用代码大致表示出来了,我们也能从中发现一些两者之间的相似点,我们做个比较与总结

比较与总结

我们看下这两者之前存在着那些不同的地方:

  1. 煮汤的主原料是不一样的,一个是排骨玉米,另一个牛肉萝卜,我们可以统称为这些为”食材”
  2. 煮的过程中添加的调味剂也不大不相同,牛肉萝卜放了好多的香料,我们也统一下,都称之为”辅料”
  3. 煮的过程中,排骨需要煮上一小时,牛肉只需要40分钟,但是动作都是”煮”

分离出了不同之处,那给这两个过程做一个统一:

  1. 原材料焯水
  2. 添加辅料
  3. 煮熟
  4. 盛碗里

发现了吧,是不是很符合我们说的模板方法模式的定义,把上面总结的统一过程作为一个实现做汤的”算法”,具体做什么汤,分别实现。现在我们就要运用模板方法来模式实现下前面两个做汤的过程,在此,你可以先忘记上面是如何实现的:

首先,我们先建立一个做汤的父类,实现这个算法:

let MakeSoup = function() {}

MakeSoup.prototype.blanching = function() {}  // 空方法,由子类重写

MakeSoup.prototype.addExcipients = function() {}  // 空方法,由子类重写

MakeSoup.prototype.cooked = function() {}  // 空方法,由子类重写

MakeSoup.prototype. intoBowl = function() {}  // 空方法,由子类重写

MakeSoup.prototype.init = function () {

    this.blanching()

    this.addExcipients()

    this.cooked()

    this.intoBowl()

}

现在这个MakeSoup类就算是实现了,但是只有这个类并不能做出什么具体的汤,因为说了,这个父类只是提供了一个抽象的算法步骤,并没有真正的意义,因此我们还要根据具体的内容实现我们所需要的内容

接下来分别实现下排骨玉米汤和牛肉萝卜这两个类:这两个类需要先继承MakeSoup,然后按照里面的步骤一步一步的重写实现

排骨玉米汤类

let Paiguyumi = new function()

Paiguyumi.prototype = new MakeSoup()

Paiguyumi.prototype.blanching = function () {
    console.info("焯排骨")
}

Paiguyumi.prototype.addExcipients = function () {
    console.info("添加生姜、料酒、玉米")
}

Paiguyumi.prototype.cooked = function () {
    console.info("煮一小时")
}

Paiguyumi.prototype.intoBowl = function () {
    console.info("排骨玉米盛碗里")
}

let paiguyumi = new Paiguyumi()

paiguyumi.init()

牛肉萝卜

let Niurouluobo = new function()

Niurouluobo.prototype = new MakeSoup()

Niurouluobo.prototype.blanching = function () {
    console.info("焯牛肉")
}

Niurouluobo.prototype.addExcipients = function () {
    console.info("加入葱姜蒜、桂皮、香叶、花椒、萝卜")
}

Niurouluobo.prototype.cooked = function () {
    console.info("煮40分钟")
}

Niurouluobo.prototype.intoBowl = function () {
    console.info("牛肉萝卜盛碗里")
}

let niurouluobo = new Niurouluobo()

niurouluobo.init()

目前我们的两个子类都模拟完了,当调用子类的init方法时,因为子类对象和构造器原型prototype上都没有对应的init方法,请求会顺着原型链找到父类的原型上对应的init方法。前面也说过了,模板方法,是封装了子类的实现算法,然后给子类的实现提供指引,告诉子类以什么样的顺序正确实执行哪些方法。在此例子中,MakeSoup.prototype.init自然便是我们的模板方法。

三、关于模板方法模式的一些解释说明

抽象类

1. 何为抽象类

抽象类往往用来表征对问题领域进行分析、设计中得出的抽象概念,是对一系列看上去不同,但是本质上相同的具体概念的抽象。抽象类是不完整的,它只能用作基类。在面向对象方法中,抽象类主要用来进行类型隐藏和充当全局变量的角色。

划重点:
  1. 对一系列看上去不同,但是本质上相同的具体概念的抽象
  2. 只能用作基类,主要用来进行类型隐藏和充当全局变量的角色

对于第一点很好理解吧,上面我们的例子也就说明了这一点,看上去两种做汤方式确实不一样,但是,归纳差异点之后,我们也能抽象出init这个模板方法,来实现本质相同。

第二点,在面向对象的语言中,抽象类是不能被实例化的,继承了某个抽象类,那么它的子类都将拥有跟抽象类一样的接口方法,是为他的子类定义公共接口。我们可以构造出一个固定的一组行为的抽象描述,但是这组行为却能够有任意个可能的具体实现方式,即它是在产生子类的同时给予子类一些特定的属性和方法。

2.javascript 模拟缺陷

实际上抽象类在JavaScript语言层面上并没有提供支持,前面我们使用的是原型继承的方式来模拟类继承,也并没有真正意义上的实现,因为在面向对象语言中,当子类继承了某个抽象类时,是必须重写父类的抽象方法,否则编译时不通过,javascript中没有这些检查,因此实现代码的时候需要人为干预,这样的做法是很不安全的,我们可以在父类的抽象方法中直接抛出一个异常:

MakeSoup.prototype.blanching = function() {
    console.error("子类必须重写blanching方法!");
}

类似的,每个抽象方法都手动抛出错误。

四、总结

模板方法模式是非常典型的用封装来提高系统拓展性的设计模式,设计了模板方法模式的代码中,子类拥有的属性和方法的执行顺序都是被确定的,在后来的拓展中,我们只要增加新的子类,就可以增加系统的新功能,而无需改动抽象类的代码。在javascript中,模拟模板方法模式固然有时候也不错,但是还有一个可能会是更好的选择,即高阶函数。