1. React 事件机制

<div onClick={this.handleClick.bind(this)}>点我</div>

React并不是将click事件绑定到了div的真实DOM上,而是在document处监听了所有的事件,当事件发生并且冒泡到document处的时候,React将事件内容封装并交由真正的处理函数运行。这样的方式不仅仅减少了内存的消耗,还能在组件挂在销毁时统一订阅和移除事件。

除此之外,冒泡到document上的事件也不是原生的浏览器事件,而是由react自己实现的合成事件(SyntheticEvent)。因此如果不想要是事件冒泡的话应该调用event.preventDefault()方法,而不是调用event.stopProppagation()方法。
JSX 上写的事件并没有绑定在对应的真实 DOM 上,而是通过事件代理的方式,将所有的事件都统一绑定在了 document 上。这样的方式不仅减少了内存消耗,还能在组件挂载销毁时统一订阅和移除事件。

另外冒泡到 document 上的事件也不是原生浏览器事件,而是 React 自己实现的合成事件(SyntheticEvent)。因此我们如果不想要事件冒泡的话,调用 event.stopPropagation 是无效的,而应该调用 event.preventDefault。

实现合成事件的目的如下:
● 合成事件首先抹平了浏览器之间的兼容问题,另外这是一个跨浏览器原生事件包装器,赋予了跨浏览器开发的能力;
● 对于原生浏览器事件来说,浏览器会给监听器创建一个事件对象。如果你有很多的事件监听,那么就需要分配很多的事件对象,造成高额的内存分配问题。但是对于合成事件来说,有一个事件池专门来管理它们的创建和销毁,当事件需要被使用时,就会从池子中复用对象,事件回调结束后,就会销毁事件对象上的属性,从而便于下次复用事件对象。

2. React的事件和普通的HTML事件有什么不同?

区别:
● 对于事件名称命名方式,原生事件为全小写,react 事件采用小驼峰;
● 对于事件函数处理语法,原生事件为字符串,react 事件为函数;
● react 事件不能采用 return false 的方式来阻止浏览器的默认行为,而必须要地明确地调用preventDefault()来阻止默认行为。

合成事件是 react 模拟原生 DOM 事件所有能力的一个事件对象,其优点如下:
● 兼容所有浏览器,更好的跨平台;
● 将事件统一存放在一个数组,避免频繁的新增与删除(垃圾回收)。
● 方便 react 统一管理和事务机制。

事件的执行顺序为原生事件先执行,合成事件后执行,合成事件会冒泡绑定到 document 上,所以尽量避免原生事件与合成事件混用,如果原生事件阻止冒泡,可能会导致合成事件不执行,因为需要冒泡到document 上合成事件才会执行。

3. React 组件中怎么做事件代理?它的原理是什么?

React基于Virtual DOM实现了一个SyntheticEvent层(合成事件层),定义的事件处理器会接收到一个合成事件对象的实例,它符合W3C标准,且与原生的浏览器事件拥有同样的接口,支持冒泡机制,所有的事件都自动绑定在最外层上。

在React底层,主要对合成事件做了两件事:
● 事件委派:React会把所有的事件绑定到结构的最外层,使用统一的事件监听器,这个事件监听器上维持了一个映射来保存所有组件内部事件监听和处理函数。
● 自动绑定:React组件中,每个方法的上下文都会指向该组件的实例,即自动绑定this为当前组件。

如何⽤webpack来优化前端性能?

⽤webpack优化前端性能是指优化webpack的输出结果,让打包的最终结果在浏览器运⾏快速⾼效。

  • 压缩代码:删除多余的代码、注释、简化代码的写法等等⽅式。可以利⽤webpack的 UglifyJsPlugin 和 ParallelUglifyPlugin 来压缩JS⽂件, 利⽤ cssnano (css-loader?minimize)来压缩css

  • 利⽤CDN加速: 在构建过程中,将引⽤的静态资源路径修改为CDN上对应的路径。可以利⽤webpack对于 output 参数和各loader的 publicPath 参数来修改资源路径

  • Tree Shaking: 将代码中永远不会⾛到的⽚段删除掉。可以通过在启动webpack时追加参数 --optimize-minimize 来实现

  • Code Splitting: 将代码按路由维度或者组件分块(chunk),这样做到按需加载,同时可以充分利⽤浏览器缓存

  • 提取公共第三⽅库: SplitChunksPlugin插件来进⾏公共模块抽取,利⽤浏览器缓存可以⻓期缓存这些⽆需频繁变动的公共代码

如何提⾼webpack的打包速度?

  • happypack: 利⽤进程并⾏编译loader,利⽤缓存来使得 rebuild 更快,遗憾的是作者表示已经不会继续开发此项⽬,类似的替代者是thread-loader
  • 外部扩展(externals): 将不怎么需要更新的第三⽅库脱离webpack打包,不被打⼊bundle中,从⽽减少打包时间,⽐如jQuery⽤script标签引⼊
  • dll: 采⽤webpack的 DllPlugin 和 DllReferencePlugin 引⼊dll,让⼀些基本不会改动的代码先打包成静态资源,避免反复编译浪费时间
  • 利⽤缓存: webpack.cache 、babel-loader.cacheDirectory、 HappyPack.cache 都可以利⽤缓存提⾼rebuild效率缩⼩⽂件搜索范围: ⽐如babel-loader插件,如果你的⽂件仅存在于src中,那么可以 include: path.resolve(__dirname,'src') ,当然绝⼤多数情况下这种操作的提升有限,除⾮不⼩⼼build了node_modules⽂件

如何提⾼webpack的构建速度?

  1. 多⼊⼝情况下,使⽤ CommonsChunkPlugin 来提取公共代码
  2. 通过 externals 配置来提取常⽤库
  3. 利⽤ DllPlugin 和 DllReferencePlugin 预编译资源模块 通过 DllPlugin 来对那些我们引⽤但是绝对不会修改的npm包来进⾏预编译,再通过 DllReferencePlugin 将预编译的模块加载进来。
  4. 使⽤ Happypack 实现多线程加速编译
  5. 使⽤ webpack-uglify-parallel 来提升 uglifyPlugin 的压缩速度。 原理上 webpack-uglify-parallel 采⽤了多核并⾏压缩来提升压缩速度
  6. 使⽤ Tree-shaking 和 Scope Hoisting 来剔除多余代码

多页面打包

webpack 多打包

经过三天的努力,终于完成了!下面就开始介绍
首先我们先介绍为什么用 webpack,webpack可以为老项目带来什么

  1. 我们的目标是,不用手动改配置,因为像老的项目,环境又多,技术又老旧。为了完成我的重构大计划,首先先节省不必要的时间,方便我们开发的同学从此不在纠结环境的问题,也方便自己,为了不出现之前的问题,第一步增加打包编译,配置文件就教给机器
  2. 开始规划,我们项目目录结构不能变,因为嵌套在 App 里面,随意改动原生获取不到文件(痛苦啊)
  3. 第一步想要之前的目录结构,单页面,完成的按照之前的结果输出
  4. 第二步增加环境变量,只需要一条命令可以解决,可以解决 一堆文件的配置
  5. 第三步可以增加压缩代码,增加代码混淆,增加 less ,sacss 等

1. 多页面打包

  1. 首先安装 webpack ,我这里安装的是
    "devDependencies": {
    "clean-webpack-plugin": "^3.0.0", 
    "copy-webpack-plugin": "^8.1.0", // 多页面打包主要工具
    "cross-env": "^7.0.3", // 命令控制主要工具
    "glob": "^7.1.6", 
    "html-webpack-plugin": "^5.3.1",
    "mini-css-extract-plugin": "^1.4.0",
    "optimize-css-assets-webpack-plugin": "^5.0.3",
    "webpack": "^5.28.0",
    "webpack-cli": "^4.6.0",
    "webpack-dev-server": "^3.10.1" // 可以开启测试环境服务
    }
  2. 第二步增加 webpack.config.js
    module.exports = {
    mode: "none",
    context: process.cwd(),
    entry: "xx/xx", // 入口文件最好是配置文件,方便环境变量
    output: {  // 出口配置
    path: 'xx',
    filename: "xxx", // 
    },
    plugins: [
    new webpack.DefinePlugin({
      "VERSION": JSON.stringify("5fa3b9"), // 此为环境变量
    }),
    new CopyWebpackPlugin({ // 下面是各个文件的输出,里面可以加入,混淆,压缩等操作
      patterns: [
        {
          from: path.join(__dirname, "app/static/"),
          to: "static",
        },
        {
          from: path.join(__dirname, "app/html/"),
          to: "html",
        },
        {
          from: path.join(__dirname, "app/static/images/"),
          to: "static/images",
        },
      ],
    }),
    ].concat(copyMultipleHtml(htmlArr)), // copyMultipleHtml 为个人封装入口配置文件自定义内容(不方便展示)
    devServer: { // 服务
    contentBase: path.resolve(__dirname, "dist"),
    host: "localhost",
    port: "8080",
    compress: false,
    },
    };
  3. 这样 webpack 打包就完成了 ,目录输出完美,我们开始增加自己的东西了
    目前我们的命令是执行

    webpack

    就可以了,现在我们需要传入参数,增加环境变量,就需要 cross-env 这个插件,首先我们安装

    npm install cross-env --save-dev

    然后我们改变命令传入环境变量 package.json 改变命令

  "scripts": {
    "prd": "cross-env NODE_ENV=prd webpack"
  },

在 webpack.config.js 里面就可以通过 process.env.NODE_ENV 渠道 prd 的参数
我们就可以像些 js 或者 node 一样 增加判断就好了... (哈哈哈)

  • 这样我们就可以在入口文件中增加 环境变量了(注:这样只能在入口文件增加的环境变量才起作用,负责需要在 CopyWebpackPlugin 这个插件里面增加)

vue3.0 Composition API 中 setup 使用

介绍

setup 简单来说就像 react 一样,你的数据定义不用放到 data, 方法不用放到 methods, 只需要一个 setup 全部搞定,不用像有些复杂的项目,接口一堆,写一会忘了上面定义的变量参数,可以一块一块来写,结合jsx 更加方便,写起来很舒服

使用

import { defineComponent, ref, reactive } from "vue";

export const VisualEditor = defineComponent({
    props: {},
    setup (props) {
                const readersNumber = ref(0)
                const book = reactive({ title: 'Vue 3 Guide' })
                const readerHtml = () => {
            <span class="text">
               hello world
            </span>
        }
        return {
                    readersNumber,
                    book,
                    readerHtml
                }
    }
})

jSON.stringify()

JSON.stringify() 方法能将一个 JavaScript 对象或值转换成一个 JSON 字符串。

第二个参数(数组)

JSOn.stringify 有第二个参数,不知道大家有没有用过,那么第二个参数作用是什么了?
例如要在控制台中打印对象的键数组。我们有一个对象 product 并且我们想知道 product 的 name 属性值。

    console.log(JSON.stringify(product));

它会输出下面的结果。

{"id":"01", name: "天启", code: "1234"}

如果数据太多在日志中很难找到 name 键,因为控制台上显示了很多没用的信息。当对象变大时,查找属性的难度增加。stringify 函数的第二个参数这时就有用了。让我们重写代码并查看结果。

    console.log(JSON.stringify(obj, ["name"]))
    // 结果
    // "{"name":"天启"}"

问题解决了,与打印整个 JSON 对象不同,我们可以在第二个参数中将所需的键作为数组传递,从而只打印所需的属性

第二个参数(函数)

我们还可以传入函数作为第二个参数。它根据函数中写入的逻辑来计算每个键值对。如果返回 undefined,则不会打印键值对。请参考示例以获得更好的理解。

const obj = {
    "name": "天启",
    "age": 999
}

JSON.stringify(obj,function (key, value) {
    if (typeof value === "string") {
        return undefined;
    }
    return value;
})

// 结果
// {"age": 26}

只有 age 被打印出来,因为函数判断 typeOf 为 String 的值返回 undefined。

第三个参数(数字)

第三个参数控制最后一个字符串的间距。如果参数是一个数字,则字符串化中的每个级别都将缩进这个数量的空格字符。

// 注意:为了达到理解的目的,使用 '--' 替代了空格
JSON.stringify(obj, null, 2)
//{
//--"name": "天启",
//--"age": 26,
//--"country": "India"
//}

第三个参数(字符串)

如果第三个参数是 string,那么将使用它来代替上面显示的空格字符。

JSON.stringify(obj, null,'**');
//{
//**"name": "天启",
//**"age": 26,
//**"country": "India"
//}
// 这里 * 取代了空格字符

toJSON 方法

我们有一个叫 toJSON 的方法,它可以作为任意对象的属性。JSON.stringify 返回这个函数的结果并对其进行序列化,而不是将整个对象转换为字符串。参考下面的例子。

const obj = {
    firstName : "天启",
    lastName : "坦克",
    age: 999,
    toJSON : function () {
        return {
            fullName: `${this.firstName} + ${this.lastName}`
        }
    }
}

console.log(JSON.stringify(obj));
// 结果
// "{"fullName": "天启坦克"}"

这里我们可以看到,它只打印 toJSON 函数的结果,而不是打印整个对象。

vue监听属性 watch

vue 监听属性 watch 有三种用法

watch 用法

普通用法

<input type="text" v-model="userName"/>
new Vue({
  el: '#box',
  data: {
    userName: '中国'
  },
  watch: {
    userName(newName, oldName) {
      // ...
    }
  }
})

vue 监听处理函数,当每次监听到 userName 值发生改变时,执行函数。

 watch: {
   userName(newName, oldName) {
     // ...
   }
 }

deep 用法

当需要监听一个对象的改变时,普通的watch方法无法监听到对象内部属性的改变,只有data中的数据才能够监听到变化,此时就需要deep属性对对象进行深度监听。

<input type="text" v-model="userName"/>
new Vue({
  el: '#box',
  data: {
    userName: {
            name: '张三',
            age: 18
        }
  },
  watch: {
    userName: {
                handler(newName, oldName) {
                    // ...
                },
                deep: true,
                immediate: true
            }
        }
  }
})

设置deep: true 则可以监听到 userName.name 的变化,此时会给 userName 的所有属性都加上这个监听器,当对象属性较多时,每个属性值的变化都会执行handler。如果只需要监听对象中的一个属性值,则可以做以下优化:使用字符串的形式监听对象属性:

watch: {
    'userName.name': {
      handler(newName, oldName) {
      // ...
      },
      deep: true,
      immediate: true
    }
  }

immediate 属性

这样使用watch时有一个特点,就是当值第一次绑定的时候,不会执行监听函数,只有值发生改变才会执行。如果我们需要在最初绑定值的时候也执行函数,则就需要用到immediate属性。
比如当父组件向子组件动态传值时,子组件props首次获取到父组件传来的默认值时,也需要执行函数,此时就需要将immediate设为true。

new Vue({
  el: '#root',
  data: {
    cityName: ''
  },
  watch: {
    cityName: {
      handler(newName, oldName) {
        // ...
      },
      immediate: true
    }
  }
})

监听的数据后面写成对象形式,包含handler方法和immediate,之前我们写的函数其实就是在写这个handler方法;

immediate表示在watch中首次绑定的时候,是否执行handler,值为true则表示在watch中声明的时候,就立即执行handler方法,值为false,则和一般使用watch一样,在数据发生变化的时候才执行handler。

渲染树构建、布局及绘制

描述下一个网页是如何渲染出来的,dom树和css树是如何合并的,浏览器的运行机制是什么,什么是否会造成渲染阻塞?

浏览器的组成

  1. 用户界面
  2. 浏览器引擎
  3. 渲染引擎
  4. 网络组件
  5. js 解析器
  6. UI 后端
  7. 数据存储持久化层

浏览器中的线程进程

浏览器使用多进程来隔离不同的网页的,没开启一个 tab 页面相当于开启一个进程,每个进程中的渲染引擎实例都是独立的,如果一个页面崩溃是不影响其他页面的。相对于线程,进程之前是不共享资源和地址栏空间的,所以会存在线程之间有可能会恶意修改或者获取非授权数据等复杂的安全问题。

请你描述下一个网页是如何渲染出来的

  1. 浏览器根据 html 资源构建 DOM 树。
  2. 浏览器根据 css 资源构建 CSSOM 树。
  3. DOM 树 和 CSS 树 合并成渲染树。
  4. 根据渲染树来布局, 计算每个节点的几何信息。
  5. 将各个节点绘制到屏幕上

dom树和css树是如何合并的

  1. 从根节点开始遍历所有可见节点
  2. 对个每个可视节点,为其找到适配的 CSSOM 规则并应用它们
  3. 构建可视节点,连同其内容和计算样式

浏览器的运行机制是什么

如何在Vue项目中使用vw实现移动端适配

有关于移动端的适配布局一直以来都是众说纷纭,对应的解决方案也是有很多种。在《使用Flexible实现手淘H5页面的终端适配》提出了Flexible的布局方案,随着viewport单位越来越受到众多浏览器的支持,因此在《再聊移动端页面的适配》一文中提出了vw来做移动端的适配问题。到目前为止不管是哪一种方案,都还存在一定的缺陷。言外之意,还没有哪一个方案是完美的。

事实上真的不完美?其实不然。最近为了新项目中能更完美的使用vw来做移动端的适配。探讨出一种能解决不兼容viewport单位的方案。今天整理一下,与大家一起分享。如果方案中存在一定的缺陷,欢迎大家一起拍正。

准备工作
对于Flexible或者说vw的布局,其原理不在这篇文章进行阐述。如果你想追踪其中的原委,强烈建议你阅读早前整理的文章《使用Flexible实现手淘H5页面的终端适配》和《再聊移动端页面的适配》。

说句题外话,由于Flexible的出现,也造成很多同学对rem的误解。正如当年大家对div的误解一样。也因此,大家都觉得rem是万能的,他能直接解决移动端的适配问题。事实并不是如此,至于为什么,我想大家应该去阅读flexible.js源码,我相信你会明白其中的原委。

回到我们今天要聊的主题,怎么实现vw的兼容问题。为了解决这个兼容问题,我将借助Vue官网提供的构建工程以及一些PostCSS插件来完成。在继续后面的内容之前,需要准备一些东西:

  • NodeJs
  • NPM
  • Webpack
  • Vue-cli
  • postcss-import
  • postcss-url
  • postcss-aspect-ratio-mini
  • postcss-cssnext
  • autoprefixer
  • postcss-px-to-viewport
  • postcss-write-svg
  • cssnano
  • postcss-viewport-units
  • Viewport Units Buggyfill
    对于这些起什么作用,先不阐述,后续我们会聊到上述的一些东西。

使用Vue-cli来构建项目

对于NodeJs、NPM和Webpack相关介绍,大家可以查阅其对应的官网。这里默认你的系统环境已经安装好Nodejs、NPM和Webpack。我的系统目前使用的Node版本是v9.4.0;NPM的版本是v5.6.0。事实上,这些都并不重要。

使用Vue-cli构建项目

为了不花太多的时间去深入的了解Webpack(Webpack对我而言,太蛋疼了),所以我直接使用Vue-cli来构建自己的项目,因为我一般使用Vue来做项目。如果你想深入的了解Webpack,建议你阅读下面的文章:

Webpack文档

Awesome Webpack
Webpack 教程资源收集
Vue+Webpack开发可复用的单页面富应用教程
接下来的内容,直接使用Vue官方提供的Vue-cli的构建工具来构建Vue项目。首先需要安装Vue-cli:

npm install -g vue-cli

全局先安装Vue-cli,假设你安装好了Vue-cli。这样就可以使用它来构建项目:

vue init webpack vw-layout

根据命令提示做相应的操作:
图片

进入到刚创建的vw-layout:

cd vw-layout

然后执行:

npm run dev

在浏览器执行http://localhost:8080,就可以看以默认的页面效果:

以前的版本需要先执行npm
i安装项目需要的依赖关系。现在新版本的可以免了。

这时,可以看到的项目结构如下:

使用Vue-cli构建项目
安装PostCSS插件
通过Vue-cli构建的项目,在项目的根目录下有一个.postcssrc.js,默认情况下已经有了:

module.exports = {
    "plugins": {
        "postcss-import": {},
        "postcss-url": {},
        "autoprefixer": {}
    }
}

对应我们开头列的的PostCSS插件清单,现在已经具备了:

  • postcss-import
  • postcss-url
  • autoprefixer

简单的说一下这几个插件。

postcss-import

postcss-import相关配置可以点击这里。目前使用的是默认配置。只在.postcssrc.js文件中引入了该插件。

postcss-import主要功有是解决@import引入路径问题。使用这个插件,可以让你很轻易的使用本地文件、node_modules或者web_modules的文件。这个插件配合postcss-url让你引入文件变得更轻松。

postcss-url

postcss-url相关配置可以点击这里。该插件主要用来处理文件,比如图片文件、字体文件等引用路径的处理。

在Vue项目中,vue-loader已具有类似的功能,只需要配置中将vue-loader配置进去。

autoprefixer

autoprefixer插件是用来自动处理浏览器前缀的一个插件。如果你配置了postcss-cssnext,其中就已具备了autoprefixer的功能。在配置的时候,未显示的配置相关参数的话,表示使用的是Browserslist指定的列表参数,你也可以像这样来指定last 2 versions 或者 > 5%。

如此一来,你在编码时不再需要考虑任何浏览器前缀的问题,可以专心撸码。这也是PostCSS最常用的一个插件之一。

其他插件
Vue-cli默认配置了上述三个PostCSS插件,但我们要完成vw的布局兼容方案,或者说让我们能更专心的撸码,还需要配置下面的几个PostCSS插件:

  • postcss-aspect-ratio-mini
  • postcss-px-to-viewport
  • postcss-write-svg
  • postcss-cssnext
  • cssnano
  • postcss-viewport-units
    要使用这几个插件,先要进行安装:
npm i postcss-aspect-ratio-mini postcss-px-to-viewport postcss-write-svg postcss-cssnext postcss-viewport-units cssnano --S   

安装成功之后,在项目根目录下的package.json文件中,可以看到新安装的依赖包:

"dependencies": {
    "cssnano": "^3.10.0",
    "postcss-aspect-ratio-mini": "0.0.2",
    "postcss-cssnext": "^3.1.0",
    "postcss-px-to-viewport": "0.0.3",
    "postcss-viewport-units": "^0.1.3",
    "postcss-write-svg": "^3.0.1",
    "vue": "^2.5.2",
    "vue-router": "^3.0.1"
},

接下来在.postcssrc.js文件对新安装的PostCSS插件进行配置:

module.exports = {
    "plugins": {
        "postcss-import": {},
        "postcss-url": {},
        "postcss-aspect-ratio-mini": {}, 
        "postcss-write-svg": {
            utf8: false
        },
        "postcss-cssnext": {},
        "postcss-px-to-viewport": {
            viewportWidth: 750,     // (Number) The width of the viewport.
            viewportHeight: 1334,    // (Number) The height of the viewport.
            unitPrecision: 3,       // (Number) The decimal numbers to allow the REM units to grow to.
            viewportUnit: 'vw',     // (String) Expected units.
            selectorBlackList: ['.ignore', '.hairlines'],  // (Array) The selectors to ignore and leave as px.
            minPixelValue: 1,       // (Number) Set the minimum pixel value to replace.
            mediaQuery: false       // (Boolean) Allow px to be converted in media queries.
        }, 
        "postcss-viewport-units":{},
        "cssnano": {
            preset: "advanced",
            autoprefixer: false,
            "postcss-zindex": false
        }
    }
}

特别声明:由于cssnext和cssnano都具有autoprefixer,事实上只需要一个,所以把默认的autoprefixer删除掉,然后把cssnano中的autoprefixer设置为false。对于其他的插件使用,稍后会简单的介绍。

由于配置文件修改了,所以重新跑一下npm run dev。项目就可以正常看到了。接下来简单的介绍一下后面安装的几个插件的作用。

postcss-cssnext

postcss-cssnext其实就是cssnext。该插件可以让我们使用CSS未来的特性,其会对这些特性做相关的兼容性处理。其包含的特性主要有:

postcss-cssnext

有关于cssnext的每个特性的操作文档,可以点击这里浏览。

cssnano

cssnano主要用来压缩和清理CSS代码。在Webpack中,cssnano和css-loader捆绑在一起,所以不需要自己加载它。不过你也可以使用postcss-loader显式的使用cssnano。有关于cssnano的详细文档,可以点击这里获取。

在cssnano的配置中,使用了preset: "advanced",所以我们需要另外安装:

npm i cssnano-preset-advanced --save-dev

cssnano集成了一些其他的PostCSS插件,如果你想禁用cssnano中的某个插件的时候,可以像下面这样操作:

"cssnano": {
    autoprefixer: false,
    "postcss-zindex": false
}

上面的代码把autoprefixer和postcss-zindex禁掉了。前者是有重复调用,后者是一个讨厌的东东。只要启用了这个插件,z-index的值就会重置为1。这是一个天坑,千万记得将postcss-zindex设置为false。

postcss-px-to-viewport

postcss-px-to-viewport插件主要用来把px单位转换为vw、vh、vmin或者vmax这样的视窗单位,也是vw适配方案的核心插件之一。

在配置中需要配置相关的几个关键参数:

"postcss-px-to-viewport": {
    viewportWidth: 750,      // 视窗的宽度,对应的是我们设计稿的宽度,一般是750
    viewportHeight: 1334,    // 视窗的高度,根据750设备的宽度来指定,一般指定1334,也可以不配置
    unitPrecision: 3,        // 指定`px`转换为视窗单位值的小数位数(很多时候无法整除)
    viewportUnit: 'vw',      // 指定需要转换成的视窗单位,建议使用vw
    selectorBlackList: ['.ignore', '.hairlines'],  // 指定不转换为视窗单位的类,可以自定义,可以无限添加,建议定义一至两个通用的类名
    minPixelValue: 1,       // 小于或等于`1px`不转换为视窗单位,你也可以设置为你想要的值
    mediaQuery: false       // 允许在媒体查询中转换`px`
}

目前出视觉设计稿,我们都是使用750px宽度的,那么100vw = 750px,即1vw = 7.5px。那么我们可以根据设计图上的px值直接转换成对应的vw值。在实际撸码过程,不需要进行任何的计算,直接在代码中写px,比如:

.test {
    border: .5px solid black;
    border-bottom-width: 4px;
    font-size: 14px;
    line-height: 20px;
    position: relative;
}
[w-188-246] {
    width: 188px;
}

编译出来的CSS:

.test {
    border: .5px solid #000;
    border-bottom-width: .533vw;
    font-size: 1.867vw;
    line-height: 2.667vw;
    position: relative;
}
[w-188-246] {
    width: 25.067vw;
}

在不想要把px转换为vw的时候,首先在对应的元素(html)中添加配置中指定的类名.ignore或.hairlines(.hairlines一般用于设置border-width:0.5px的元素中):

<div class="box ignore"></div>

写CSS的时候:

.ignore {
    margin: 10px;
    background-color: red;
}
.box {
    width: 180px;
    height: 300px;
}
.hairlines {
    border-bottom: 0.5px solid red;
}

编译出来的CSS:

.box {
    width: 24vw;
    height: 40vw;
}
.ignore {
    margin: 10px; /*.box元素中带有.ignore类名,在这个类名写的`px`不会被转换*/
    background-color: red;
}
.hairlines {
    border-bottom: 0.5px solid red;
}

上面解决了px到vw的转换计算。那么在哪些地方可以使用vw来适配我们的页面。根据相关的测试:

容器适配,可以使用vw
文本的适配,可以使用vw
大于1px的边框、圆角、阴影都可以使用vw
内距和外距,可以使用vw
postcss-aspect-ratio-mini
postcss-aspect-ratio-mini主要用来处理元素容器宽高比。在实际使用的时候,具有一个默认的结构

<div aspectratio>
    <div aspectratio-content></div>
</div>

在实际使用的时候,你可以把自定义属性aspectratio和aspectratio-content换成相应的类名,比如:

<div class="aspectratio">
    <div class="aspectratio-content"></div>
</div>

我个人比较喜欢用自定义属性,它和类名所起的作用是同等的。结构定义之后,需要在你的样式文件中添加一个统一的宽度比默认属性:

[aspectratio] {
    position: relative;
}
[aspectratio]::before {
    content: '';
    display: block;
    width: 1px;
    margin-left: -1px;
    height: 0;
}

[aspectratio-content] {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    width: 100%;
    height: 100%;
}

如果我们想要做一个188:246(188是容器宽度,246是容器高度)这样的比例容器,只需要这样使用:

[w-188-246] {
    aspect-ratio: '188:246';
}

有一点需要特别注意:aspect-ratio属性不能和其他属性写在一起,否则编译出来的属性只会留下aspect-ratio的值,比如:

<div aspectratio w-188-246 class="color"></div>

编译前的CSS如下:

[w-188-246] {
    width: 188px;
    background-color: red;
    aspect-ratio: '188:246';
}

编译之后:

[w-188-246]:before {
    padding-top: 130.85106382978725%;
}

主要是因为在插件中做了相应的处理,不在每次调用aspect-ratio时,生成前面指定的默认样式代码,这样代码没那么冗余。所以在使用的时候,需要把width和background-color分开来写:

[w-188-246] {
    width: 188px;
    background-color: red;
}
[w-188-246] {
    aspect-ratio: '188:246';
}

这个时候,编译出来的CSS就正常了:

[w-188-246] {
    width: 25.067vw;
    background-color: red;
}
[w-188-246]:before {
    padding-top: 130.85106382978725%;
}

有关于宽高比相关的详细介绍,如果大家感兴趣的话,可以阅读下面相关的文章:

CSS实现长宽比的几种方案
容器长宽比
Web中如何实现纵横比
实现精准的流体排版原理
目前采用PostCSS插件只是一个过渡阶段,在将来我们可以直接在CSS中使用aspect-ratio属性来实现长宽比。

postcss-write-svg
postcss-write-svg插件主要用来处理移动端1px的解决方案。该插件主要使用的是border-image和background来做1px的相关处理。比如:

@svg 1px-border {
    height: 2px;
    @rect {
        fill: var(--color, black);
        width: 100%;
        height: 50%;
    }
}
.example {
    border: 1px solid transparent;
    border-image: svg(1px-border param(--color #00b1ff)) 2 2 stretch;
}

编译出来的CSS:

.example {
    border: 1px solid transparent;
    border-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' height='2px'%3E%3Crect fill='%2300b1ff' width='100%25' height='50%25'/%3E%3C/svg%3E") 2 2 stretch;
}

上面演示的是使用border-image方式,除此之外还可以使用background-image来实现。比如:

@svg square {
    @rect {
        fill: var(--color, black);
        width: 100%;
        height: 100%;
    }
}

#example {
    background: white svg(square param(--color #00b1ff));
}

编译出来就是:

#example {
    background: white url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg'%3E%3Crect fill='%2300b1ff' width='100%25' height='100%25'/%3E%3C/svg%3E");
}

解决1px的方案除了这个插件之外,还有其他的方法。可以阅读前期整理的《再谈Retina下1px的解决方案》一文。

特别声明:由于有一些低端机对border-image支持度不够友好,个人建议你使用background-image的这个方案。

CSS Modules

Vue中的vue-loader已经集成了CSS Modules的功能,个人建议在项目中开始使用CSS Modules。特别是在Vue和React的项目中,CSS Modules具有很强的优势和灵活性。建议看看CSS In JS相关的资料。在Vue中,使用CSS Modules的相关文档可以阅读Vue官方提供的文档《CSS Modules》。

postcss-viewport-units

postcss-viewport-units插件主要是给CSS的属性添加content的属性,配合viewport-units-buggyfill库给vw、vh、vmin和vmax做适配的操作。

这是实现vw布局必不可少的一个插件,因为少了这个插件,这将是一件痛苦的事情。后面你就清楚。

到此为止,有关于所需要的PostCSS已配置完。并且简单的介绍了各个插件的作用,至于详细的文档和使用,可以参阅对应插件的官方文档。

vw兼容方案
在《再聊移动端页面的适配》一文中,详细介绍了,怎么使用vw来实现移动端的适配布局。这里不做详细的介绍。建议你花点时间阅读这篇文章。

先把未做兼容处理的示例二维码贴一个:

你可以使用手淘App、优酷APP、各终端自带的浏览器、UC浏览器、QQ浏览器、Safari浏览器和Chrome浏览器扫描上面的二维码,您看到相应的效果:

但还有不支持的,比如下表中的No,表示的就是不支持

品牌 型号 系统版本 分辨率 屏幕尺寸 手淘APP 优酷APP 原生浏览器 QQ浏览器 UC浏览器 Chrome浏览器
华为 Mate9 Android7.0 1080 x 1920 5英寸 Yes Yes No Yes Yes Yes
华为 Mate7 Android4.2 1080 x 1920 5.2英寸 Yes Yes No Yes Yes Yes
魅族 Mx4 (M460 移动4G) Android4.4.2 1152 x 1920 5.36英寸 Yes No No Yes Yes Yes
Oppo R7007 Android4.3 1280 x 720 5英寸 Yes No No Yes Yes No
三星 N9008 (Galaxy Note3) Android4.4.2 1080 x 1920 5.7英寸 Yes No Yes Yes Yes Yes

华硕 | ZenFone5(x86) Android4.3 720 x 280 5英寸 No No No Yes No No
正因如此,很多同学都不敢尝这个螃蟹。害怕去处理兼容性的处理。不过不要紧,今天我把最终的解决方案告诉你。

最终的解决方案,就是使用viewport的polyfill:Viewport Units Buggyfill。使用viewport-units-buggyfill主要分以下几步走:

引入JavaScript文件
viewport-units-buggyfill主要有两个JavaScript文件:viewport-units-buggyfill.jsviewport-units-buggyfill.hacks.js。你只需要在你的HTML文件中引入这两个文件。比如在Vue项目中的index.html引入它们:

<script src="//g.alicdn.com/fdilab/lib3rd/viewport-units-buggyfill/0.6.2/??viewport-units-buggyfill.hacks.min.js,viewport-units-buggyfill.min.js"></script>

你也可以使用其他的在线CDN地址,也可将这两个文件合并压缩成一个.js文件。这主要看你自己的兴趣了。

第二步,在HTML文件中调用viewport-units-buggyfill,比如:

<script>
    window.onload = function () {
        window.viewportUnitsBuggyfill.init({
            hacks: window.viewportUnitsBuggyfillHacks
        });
    }
</script>

为了你Demo的时候能获取对应机型相关的参数,我在示例中添加了一段额外的代码,估计会让你有点烦:

<script>
    window.onload = function () {
        window.viewportUnitsBuggyfill.init({
        hacks: window.viewportUnitsBuggyfillHacks
        });

        var winDPI = window.devicePixelRatio;
        var uAgent = window.navigator.userAgent;
        var screenHeight = window.screen.height;
        var screenWidth = window.screen.width;
        var winWidth = window.innerWidth;
        var winHeight = window.innerHeight;

        alert(
            "Windows DPI:" + winDPI +
            ";\ruAgent:" + uAgent +
            ";\rScreen Width:" + screenWidth +
            ";\rScreen Height:" + screenHeight +
            ";\rWindow Width:" + winWidth +
            ";\rWindow Height:" + winHeight
        )
    }
</script>

具体的使用。在你的CSS中,只要使用到了viewport的单位(vw、vh、vmin或vmax )地方,需要在样式中添加content:

.my-viewport-units-using-thingie {
    width: 50vmin;
    height: 50vmax;
    top: calc(50vh - 100px);
    left: calc(50vw - 100px);

    /* hack to engage viewport-units-buggyfill */
    content: 'viewport-units-buggyfill; width: 50vmin; height: 50vmax; top: calc(50vh - 100px); left: calc(50vw - 100px);';
}

这可能会令你感到恶心,而且我们不可能每次写vw都去人肉的计算。特别是在我们的这个场景中,咱们使用了postcss-px-to-viewport这个插件来转换vw,更无法让我们人肉的去添加content内容。

这个时候就需要前面提到的postcss-viewport-units插件。这个插件将让你无需关注content的内容,插件会自动帮你处理。比如插件处理后的代码:

![](https://upload-i

mages.jianshu.io/upload_images/9159664-c8ce5d8618b11c24..png?imageMogr2/auto-orient/strip%7CimageView2/2/w/784/format/webp)

Viewport Units Buggyfill还提供了其他的功能。详细的这里不阐述了。但是content也会引起一定的副作用。比如img和伪元素::before(:before)或::after(:after)。在img中content会引起部分浏览器下,图片不会显示。这个时候需要全局添加:

img {
    content: normal !important;
}

而对于::after之类的,就算是里面使用了vw单位,Viewport Units Buggyfill对其并不会起作用。比如:

// 编译前
.after {
    content: 'after content';
    display: block;
    width: 100px;
    height: 20px;
    background: green;
}

// 编译后
.after[data-v-469af010] {
    content: "after content";
    display: block;
    width: 13.333vw;
    height: 2.667vw;
    background: green;
}

这个时候我们需要通过添加额外的标签来替代伪元素(这个情景我没有测试到,后面自己亲测一下)。

到了这个时候,你就不需要再担心兼容问题了。比如下面这个示例:

请用你的手机,不管什么APP扫一扫,你就可以看到效果。(小心弹框哟),如果你发现了还是有问题,请把弹出来的信息截图发给我。

如查你想看看别的机型效果,可以点击这里、这里、这里、还有这里。整个示例的源码,可以点击这里下载。

如果你下载了示你源码,先要确认你的系统环境能跑Vue的项目,然后下载下来之后,解压缩,接着运行npm i,再运行npm run dev,你就可以看到效果了。

总结
如果你看到这里了,希望这篇文章对你有所帮助。能帮助你解决项目中的实际问题,让你不再担心移动端的适配问题。当然更希望的是你在实际的项目中用起这个方案,把碰到的问题及时反馈给偶。如果你有更好的方案,欢迎在下面的评论中与我们一起分享。

著作权归作者所有。
商业转载请联系作者获得授权,非商业转载请注明出处。
原文: https://www.w3cplus.com/mobile/vw-layout-in-vue.html © w3cplus.com著作权归作者所有。
商业转载请联系作者获得授权,非商业转载请注明出处。
原文: https://www.w3cplus.com/mobile/vw-layout-in-vue.html © w3cplus.com著作权归作者所有。
商业转载请联系作者获得授权,非商业转载请注明出处。
原文: https://www.w3cplus.com/mobile/vw-layout-in-vue.html © w3cplus.com

IOS img标签下图片无法显示

IOS img 图片无法展示几种原因

  1. 定位问题,相同标签下 flex 布局加 position 定位出现问题,解决办法,选择一种
  2. 图片 src 不显示,base64 和 网络图片地址都不显示,解决办法, 给 img 添加父盒子,img 宽高设置 100%;
代码:
图片 src 不显示,base64 和 网络图片地址都不显示,解决办法, 给 img 添加父盒子,img 宽高设置 100%;
    <div>
        <img src="http://www.dz1995.com/wp-content/uploads/2017/03/bf28b7195f6f948df8c9d3637ef5a3f68fadb11849cc2-0XAbEw_fw658.jpg">
    </div>
    div {
        width: 200px;
        height: 200px;
    }

    div img {
        width: 100%;
        height: 100%;
    }

JavaScript 运行机制:Event Loop(转)

一、为什么 JavaScript 是单线程?

JavaScript 是单线程运行的,也就是说同一时间只能干一件事。那么为什么 JavaScript 不能是多线程呢?这样可以高效运行啊!

首先 JavaScript 单线程和它的用的有关,作为浏览器脚本语言,他的用途是和用户互动以及操作 DOM 。这决定了它只能是单线程运行,否则会带来很复杂的同步问题。比如,JavaScript 有个俩个线程,一个线程在某个 DOM 节点上添加内容,另一个线程删除了这个节点,这是浏览器应该以哪个线程为主?

所以避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这问语言的核心特征,将来也不会改变。

为了利用多核 CPU 的计算能力,HTML5 提出一个 Web Worker 标准,允许 JavaScript 脚本创建多个线程,但是子线程完全受主线程控制,且不得操作 DOM ,所以这个新的标准并没有改变 JavaScript 单线程的本质。

二、任务队列

单线程就意味着,所有任务需要排队,前一个任务结束,才会执行下一个任务,如果前一个任务耗时很长,后一个任务就不得不一直等着。

如果排队是因为计算量大,CPU忙不过来,倒也算了,但是很多时候CPU是闲着的,因为IO设备(输入输出设备)很慢(比如Ajax操作从网络读取数据),不得不等着结果出来,再往下执行。

JavaScript语言的设计者意识到,这时主线程完全可以不管IO设备,挂起处于等待中的任务,先运行排在后面的任务。等到IO设备返回了结果,再回过头,把挂起的任务继续执行下去。

于是,所有任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。

具体来说,异步执行的运行机制如下。(同步执行也是如此,因为它可以被视为没有异步任务的异步执行。)

(1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。

(2)主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。

(3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。

(4)主线程不断重复上面的第三步。

下图就是主线程和任务队列的示意图。
img
只要主线程空了,就会去读取"任务队列",这就是JavaScript的运行机制。这个过程会不断重复。


三、事件和回调函数

"任务队列"是一个事件的队列(也可以理解成消息的队列),IO设备完成一项任务,就在"任务队列"中添加一个事件,表示相关的异步任务可以进入"执行栈"了。主线程读取"任务队列",就是读取里面有哪些事件。

"任务队列"中的事件,除了IO设备的事件以外,还包括一些用户产生的事件(比如鼠标点击、页面滚动等等)。只要指定过回调函数,这些事件发生时就会进入"任务队列",等待主线程读取。

所谓"回调函数"(callback),就是那些会被主线程挂起来的代码。异步任务必须指定回调函数,当主线程开始执行异步任务,就是执行对应的回调函数。

"任务队列"是一个先进先出的数据结构,排在前面的事件,优先被主线程读取。主线程的读取过程基本上是自动的,只要执行栈一清空,"任务队列"上第一位的事件就自动进入主线程。但是,由于存在后文提到的"定时器"功能,主线程首先要检查一下执行时间,某些事件只有到了规定的时间,才能返回主线程。

四、Event Loop

主线程从"任务队列"中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)。

为了更好地理解Event Loop,请看下图(转引自Philip Roberts的演讲《Help, I'm stuck in an event-loop》)。
img
上图中,主线程运行的时候,产生堆(heap)和栈(stack),栈中的代码调用各种外部API,它们在"任务队列"中加入各种事件(click,load,done)。只要栈中的代码执行完毕,主线程就会去读取"任务队列",依次执行那些事件所对应的回调函数。

执行栈中的代码(同步任务),总是在读取"任务队列"(异步任务)之前执行。请看下面这个例子。

    var req = new XMLHttpRequest();
    req.open('GET', url);    
    req.onload = function (){};    
    req.onerror = function (){};    
    req.send();

上面代码中的req.send方法是Ajax操作向服务器发送数据,它是一个异步任务,意味着只有当前脚本的所有代码执行完,系统才会去读取"任务队列"。所以,它与下面的写法等价。

    var req = new XMLHttpRequest();
    req.open('GET', url);
    req.send();
    req.onload = function (){};    
    req.onerror = function (){};   

也就是说,指定回调函数的部分(onload和onerror),在send()方法的前面或后面无关紧要,因为它们属于执行栈的一部分,系统总是执行完它们,才会去读取"任务队列"。


五、定时器

除了放置异步任务的事件,"任务队列"还可以放置定时事件,即指定某些代码在多少时间之后执行。这叫做"定时器"(timer)功能,也就是定时执行的代码。

定时器功能主要由setTimeout()和setInterval()这两个函数来完成,它们的内部运行机制完全一样,区别在于前者指定的代码是一次性执行,后者则为反复执行。以下主要讨论setTimeout()。

setTimeout()接受两个参数,第一个是回调函数,第二个是推迟执行的毫秒数。

console.log(1);
setTimeout(function(){console.log(2);},1000);
console.log(3);

上面代码的执行结果是1,3,2,因为setTimeout()将第二行推迟到1000毫秒之后执行。

如果将setTimeout()的第二个参数设为0,就表示当前代码执行完(执行栈清空)以后,立即执行(0毫秒间隔)指定的回调函数。

setTimeout(function(){console.log(1);}, 0);
console.log(2);

上面代码的执行结果总是2,1,因为只有在执行完第二行以后,系统才会去执行"任务队列"中的回调函数。

总之,setTimeout(fn,0)的含义是,指定某个任务在主线程最早可得的空闲时间执行,也就是说,尽可能早得执行。它在"任务队列"的尾部添加一个事件,因此要等到同步任务和"任务队列"现有的事件都处理完,才会得到执行。

HTML5标准规定了setTimeout()的第二个参数的最小值(最短间隔),不得低于4毫秒,如果低于这个值,就会自动增加。在此之前,老版本的浏览器都将最短间隔设为10毫秒。另外,对于那些DOM的变动(尤其是涉及页面重新渲染的部分),通常不会立即执行,而是每16毫秒执行一次。这时使用requestAnimationFrame()的效果要好于setTimeout()。

需要注意的是,setTimeout()只是将事件插入了"任务队列",必须等到当前代码(执行栈)执行完,主线程才会去执行它指定的回调函数。要是当前代码耗时很长,有可能要等很久,所以并没有办法保证,回调函数一定会在setTimeout()指定的时间执行。

六、Node.js的Event Loop

Node.js也是单线程的Event Loop,但是它的运行机制不同于浏览器环境。

请看下面的示意图(作者@BusyRich)。

img

根据上图,Node.js的运行机制如下。

(1)V8 引擎解析 JavaScript 脚本。

(2)解析后的代码,调用 Node API。

(3)libuv 库负责 Node API 的执行。它将不同的任务分配给不同的线程,形成一个Event Loop(事件循环),以异步的方式将任务的执行结果返回给V8引擎。

(4)V8 引擎再将结果返回给用户。

除了setTimeout和setInterval这两个方法,Node.js还提供了另外两个与"任务队列"有关的方法:process.nextTick和setImmediate。它们可以帮助我们加深对"任务队列"的理解。

process.nextTick方法可以在当前"执行栈"的尾部----下一次Event Loop(主线程读取"任务队列")之前----触发回调函数。也就是说,它指定的任务总是发生在所有异步任务之前。setImmediate方法则是在当前"任务队列"的尾部添加事件,也就是说,它指定的任务总是在下一次Event Loop时执行,这与setTimeout(fn, 0)很像。请看下面的例子(via StackOverflow)。

process.nextTick(function A() {
  console.log(1);
  process.nextTick(function B(){console.log(2);});
});

setTimeout(function timeout() {
  console.log('TIMEOUT FIRED');
}, 0)
// 1
// 2
// TIMEOUT FIRED

上面代码中,由于process.nextTick方法指定的回调函数,总是在当前"执行栈"的尾部触发,所以不仅函数A比setTimeout指定的回调函数timeout先执行,而且函数B也比timeout先执行。这说明,如果有多个process.nextTick语句(不管它们是否嵌套),将全部在当前"执行栈"执行。

现在,再看setImmediate。

setImmediate(function A() {
  console.log(1);
  setImmediate(function B(){console.log(2);});
});

setTimeout(function timeout() {
  console.log('TIMEOUT FIRED');
}, 0);

上面代码中,setImmediate与setTimeout(fn,0)各自添加了一个回调函数A和timeout,都是在下一次Event Loop触发。那么,哪个回调函数先执行呢?答案是不确定。运行结果可能是1--TIMEOUT FIRED--2,也可能是TIMEOUT FIRED--1--2。

令人困惑的是,Node.js文档中称,setImmediate指定的回调函数,总是排在setTimeout前面。实际上,这种情况只发生在递归调用的时候。

setImmediate(function (){
  setImmediate(function A() {
    console.log(1);
    setImmediate(function B(){console.log(2);});
  });

  setTimeout(function timeout() {
    console.log('TIMEOUT FIRED');
  }, 0);
});
// 1
// TIMEOUT FIRED
// 2

上面代码中,setImmediate和setTimeout被封装在一个setImmediate里面,它的运行结果总是1--TIMEOUT FIRED--2,这时函数A一定在timeout前面触发。至于2排在TIMEOUT FIRED的后面(即函数B在timeout后面触发),是因为setImmediate总是将事件注册到下一轮Event Loop,所以函数A和timeout是在同一轮Loop执行,而函数B在下一轮Loop执行。

我们由此得到了process.nextTick和setImmediate的一个重要区别:多个process.nextTick语句总是在当前"执行栈"一次执行完,多个setImmediate可能则需要多次loop才能执行完。事实上,这正是Node.js 10.0版添加setImmediate方法的原因,否则像下面这样的递归调用process.nextTick,将会没完没了,主线程根本不会去读取"事件队列"!

process.nextTick(function foo() {
  process.nextTick(foo);
});

事实上,现在要是你写出递归的process.nextTick,Node.js会抛出一个警告,要求你改成setImmediate。

另外,由于process.nextTick指定的回调函数是在本次"事件循环"触发,而setImmediate指定的是在下次"事件循环"触发,所以很显然,前者总是比后者发生得早,而且执行效率也高(因为不用检查"任务队列")。

js 垃圾回收机制

内存的生命周期

内存分配 -> 使用内存 -> 释放内存

js 环境中分配内存有如下声明周期:
  1. 内存分配:在我们申明变量,函数,对象的时候,系统会为我们自动分配内存。
  2. 内存使用:读写内存,也就是使用变量,函数等。
  3. 释放内存:使用完毕,由垃圾回收机制自动回收不在使用的内存。

js 内存的分配

为了让程序员不在费劲的分配内存,JavaScript 在定义变量的时候就完成了内存分配。
    var n = 123; // 给数值变量分配内存
    var s = "azerty"; // 给字符串分配内存

    var o = {
        a: 1,
        b: null
    }; // 给对象及其包含的值分配内存

    // 给数组及其包含的值分配内存(就像对象一样)
    var a = [1, null, "abra"]; 

    function f(a){
        return a + 2;
    } // 给函数(可调用的对象)分配内存

    // 函数表达式也能分配一个对象
    someElement.addEventListener('click', function(){
        someElement.style.backgroundColor = 'blue';
    }, false);
有些函数调用结果是分配函数的内存:
    var date = new Date(); // 分配一个 Date 对象

    var ele = document.createElement("div"); // 分配一个 DOM 对象
有些方法分配新的变量或者新对象:
    var s = "asdfsfd"
    var s2 = s.substr(0,3) // s2 是一个新的字符串
    // 因为字符串是不变量
    // JavaScript 可能决定不分配内存
    // 只是存储了[0-3] 的范围

    var a = ['abc', 'cbd'];
    var a2 = ['box', 'abs'];
    var a3 = a.concat(a2);
    // 新数组有四个元素, 是 a 连接 a2 的结果

js 内存的使用

使用值的过程实际上是对分配内存进行读取和写入操作。读取与写入可能是写入一个变量或者一个对象的属性值,甚至传递函数的参数。
    var a = 10; // 分配内存
    console.log(a); // 对内存的读取使用

js 的内存回收

js 有自动垃圾回收机制, 那么这个垃圾自动回收机制的原理是什么呢? 其实很简单,就是找出那些不再继续使用的值,然后释放其内存。
多少内存管理的问题都在这个阶段。在这里最艰的任务是找到不再需要使用的变量。
不再需要使用的变量也就是在生命周期结束的变量,是局部变量,局部变量只在函数的执行过程中存在,当函数运行结束,没有其他引用(闭包),那么该变量会被标记回收。
全局变量的生命周期直至浏览器卸载页面才会结束,也就是说全局变量不会被当成垃圾回收。
因为自动垃圾回收机制的存在,开发人员可以不关系也不注意内存释放的有关问题,但是无用内存释放这件事是客观存在的。不幸的是,即使不考虑垃圾回收对性能的影响,目前最新的垃圾回收算法,也无法智能回收所有的极端情况。

垃圾回收

引用

垃圾回收算法主要依赖于引用的概念
在内存管理感觉中,一个对象如果有访问另一个对象的权限(显示或者隐式),叫做一个对象引用另一个对象。
例如,一个javascript 对象具有对他原型的引用(隐式引用)和对它属性的引用(显示引用)。
在这里,“对象”的概念不仅特指 JavaScript 对象,还包括函数作用域(或者全局词法作用域)

引用计数垃圾收集

这是最初的垃圾回收算法。
引用计数算法定义“内存不再使用”的标准很简单,就是看一个对象是否有指向它的引用。如果没有其他对象指向它了,说明对象已经不再需要了。
    var o = {
        a: {
            b: 2
        }
    };
    // 两个对象被创建, 一个作为另一个的属性被引用, 另一个被分配给变量。
    // 很显然,没有一个可以被垃圾收集

    var o2 = o; // o2 变量是第二个对“对象”的引用

    o = 1; // 现在, “这个对象” 的原始引用 o 被 o2 替换了

    var oa = o2.a; // 引用“这个对象”的 a 属性
    // 现在,“这个对象”有两个引用了, 一个是 o2, 一个是 oa

    o2 = "ya"; // 最初的对象现在已经是零引用了
                         // 它可以被垃圾回收了
                         // 然后它的属性 a 的对象还在被 oa 引用, 所以还不能回收
    oa = null; // a 属性的那个喜爱那个现在也是零引用了
                         // 它可以被垃圾回收了
由上面可以看出,引用计算算法是个简单有效的算法,但它却存在一个致命的问题: 循环引用。
如果两个对象相互引用,尽管他们已不再使用,垃圾回收不会进行回收,导致内存泄露。
来看一个循环引用的例子:
    function f() {
        var o = {};
        var o2 = {};
        o.a = a2; // o 引用 o2
        o2.a = o; // o2 引用 o 这里

        return "azerty";
    }

    f();
上面我们申明了一个函数 f,其中包含两个相互引用的对象。在调用函数结束后,对象 o1 和 o2 实际已离开函数范围,因此不再需要了。但根据引用计数的原则,他们之间的相互引用依然存在,因此这部分内存不会被回收,内存泄露不可避免了。
再来看一个实际的例子:
    var div = document.createElement("div");
    div.onclick = function() {
            console.log("click");
    };
上面这种JS写法再普通不过了,创建一个DOM元素并绑定一个点击事件。 此时变量 div 有事件处理函数的引用,同时事件处理函数也有div的引用!(div变量可在函数内被访问)。 一个循序引用出现了,按上面所讲的算法,该部分内存无可避免的泄露了。
为了解决循环引用造成的问题,现代浏览器通过使用标记清除算法来实现垃圾回收。

标记清除算法

标记清除算法将“不再使用的对象”定义为“无法达到的对象”。 简单来说,就是从根部(在JS中就是全局对象)出发定时扫描内存中的对象。 凡是能从根部到达的对象,都是还需要使用的。 那些无法由根部出发触及到的对象被标记为不再使用,稍后进行回收。
从这个概念可以看出,无法触及的对象包含了没有引用的对象这个概念(没有任何引用的对象也是无法触及的对象)。 但反之未必成立。

工作流程

  1. 垃圾收集器会在运行的时候会给存储在内存中的所有变量都加上标记。
  2. 从根部出发将能触及到的对象的标记清除。
  3. 那些还存在标记的变量被视为准备删除的变量。
  4. 最后垃圾收集器会执行最后一步内存清除的工作,销毁那些带标记的值并回收它们所占用的内存空间。

图片

循环引用不再是问题了

在看之前循环引用的例子:
function f(){
  var o = {};
  var o2 = {};
  o.a = o2; // o 引用 o2
  o2.a = o; // o2 引用 o

  return "azerty";
}

f();

函数调用返回之后,两个循环引用的对象在垃圾收集时从全局对象出发无法再获取他们的引用。 因此,他们将会被垃圾回收器回收。

内存泄露泄漏

程序的运行需要内存。只要程序提出要求,操作系统或者运行时(runtime)就必须供给内存。

对于持续运行的服务进程(daemon),必须及时释放不再用到的内存。
否则,内存占用越来越高,轻则影响系统性能,重则导致进程崩溃。

本质上讲,内存泄漏就是由于疏忽或错误造成程序未能释放那些已经不再使用的内存,造成内存的浪费。

内存泄漏的识别方法

经验法则是,如果连续五次垃圾回收之后,内存占用一次比一次大,就有内存泄漏。 这就要求实时查看内存的占用情况。

在 Chrome 浏览器中,我们可以这样查看内存占用情况

  1. 打开开发者工具,选择 Performance 面板
  2. 在顶部勾选 Memory
  3. 点击左上角的 record 按钮
  4. 在页面上进行各种操作,模拟用户的使用情况
  5. 一段时间后,点击对话框的 stop 按钮,面板上就会显示这段时间的内存占用情况
来看一张效果图:

我们有两种方式来判定当前是否有内存泄漏:

  1. 多次快照后,比较每次快照中内存的占用情况,如果呈上升趋势,那么可以认为存在内存泄漏
  2. 某次快照后,看当前内存占用的趋势图,如果走势不平稳,呈上升趋势,那么可以认为存在内存泄漏

在服务器环境中使用 Node 提供的 process.memoryUsage 方法查看内存情况

console.log(process.memoryUsage());
// { 
//     rss: 27709440,
//     heapTotal: 5685248,
//     heapUsed: 3449392,
//     external: 8772 
// }

process.memoryUsage返回一个对象,包含了 Node 进程的内存占用信息。
该对象包含四个字段,单位是字节,含义如下:

  • rss(resident set size):所有内存占用,包括指令区和堆栈。
  • heapTotal:"堆"占用的内存,包括用到的和没用到的。
  • heapUsed:用到的堆的部分。
  • external: V8 引擎内部的 C++ 对象占用的内存。

判断内存泄漏,以heapUsed字段为准。

意外的全局变量
function foo() {
    bar1 = 'some text'; // 没有声明变量 实际上是全局变量 => window.bar1
    this.bar2 = 'some text' // 全局变量 => window.bar2
}
foo();

在这个例子中,意外的创建了两个全局变量 bar1 和 bar2

被遗忘的定时器和回调函数

在很多库中, 如果使用了观察者模式, 都会提供回调方法, 来调用一些回调函数。 要记得回收这些回调函数。举一个 setInterval的例子:
var serverData = loadData();
setInterval(function() {
    var renderer = document.getElementById('renderer');
    if(renderer) {
        renderer.innerHTML = JSON.stringify(serverData);
    }
}, 5000); // 每 5 秒调用一次

如果后续 renderer 元素被移除,整个定时器实际上没有任何作用。 但如果你没有回收定时器,整个定时器依然有效, 不但定时器无法被内存回收, 定时器函数中的依赖也无法回收。在这个案例中的 serverData 也无法被回收。

闭包

在 JS 开发中,我们会经常用到闭包,一个内部函数,有权访问包含其的外部函数中的变量。 下面这种情况下,闭包也会造成内存泄露:
    var theThing = null;
var replaceThing = function () {
  var originalThing = theThing;
  var unused = function () {
    if (originalThing) // 对于 'originalThing'的引用
      console.log("hi");
  };
  theThing = {
    longStr: new Array(1000000).join('*'),
    someMethod: function () {
      console.log("message");
    }
  };
};
setInterval(replaceThing, 1000);

这段代码,每次调用 replaceThing 时,theThing 获得了包含一个巨大的数组和一个对于新闭包 someMethod 的对象。
同时 unused 是一个引用了 originalThing 的闭包。
这个范例的关键在于,闭包之间是共享作用域的,尽管 unused 可能一直没有被调用,但是 someMethod 可能会被调用,就会导致无法对其内存进行回收。
当这段代码被反复执行时,内存会持续增长。

DOM 引用

很多时候, 我们对 Dom 的操作, 会把 Dom 的引用保存在一个数组或者 Map 中。

var elements = {
    image: document.getElementById('image')
};
function doStuff() {
    elements.image.src = 'http://example.com/image_name.png';
}
function removeImage() {
    document.body.removeChild(document.getElementById('image'));
    // 这个时候我们对于 #image 仍然有一个引用, Image 元素, 仍然无法被内存回收.
}

上述案例中,即使我们对于 image 元素进行了移除,但是仍然有对 image 元素的引用,依然无法对齐进行内存回收。
另外需要注意的一个点是,对于一个 Dom 树的叶子节点的引用。
举个例子: 如果我们引用了一个表格中的td元素,一旦在 Dom 中删除了整个表格,我们直观的觉得内存回收应该回收除了被引用的 td 外的其他元素。
但是事实上,这个 td 元素是整个表格的一个子元素,并保留对于其父元素的引用。
这就会导致对于整个表格,都无法进行内存回收。所以我们要小心处理对于 Dom 元素的引用。

如何避免内存泄漏

记住一个原则:不用的东西,及时归还。
  1. 减少不必要的全局变量,使用严格模式避免意外创建全局变量。
  2. 在你使用完数据后,及时解除引用(闭包中的变量,dom引用,定时器清除)。
  3. 组织好你的逻辑,避免死循环等造成浏览器卡顿,崩溃的问题。

参考

MDN-内存管理
JavaScript高级程序设计
JavaScript权威指南
JavaScript 内存泄漏教程
一种有趣的JavaScript内存泄漏
内存泄露

小程序开发

  1. 小程序看起来和 html + CSS + javascript 但是实际上深入后就会发现他们有很多的不同,这里面的坑就比较多了,比如小程序对 wxss 对标准的 css 支持多少没有明确文档说明,有时候出了问题,需要多方面考虑,就像我刚刚遇到一个问题 (mpvue 开发小程序) :class="{action:isAce, unaction: !isAce}",在低版本测试中发现样式并没有出来,看到 dom 结构上 class=",unaction" 出现这样情况很无奈啊,但是 1.6 以上基础库就没有这问题。类似这样的坑还有好多,比如多次点击,事件跳转打开多个层
  2. 小程序中项目开发中有很多问题和难点,需要多方面的考虑,像 mpvue 中午生命周期没有触发等

  1. 适合H5页面,兼容ie10+,图片base64显示,主要功能点是FileReader和readAsDataURL
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>files-h5</title>
</head>
<body>
  <input type="file" id="file" onchange="showPreview(this, 'portrait')" />
  <img src="" id="portrait" style="width: 200px; height: 200px; display: block;" />
  <script>
  function showPreview(source, imgId) {
    var file = source.files[0];
    if(window.FileReader) {
      var fr = new FileReader();
      fr.onloadend = function(e) {
        document.getElementById(imgId).src = e.target.result;
      }
      fr.readAsDataURL(file);
    }
  }
  </script>
</body>
</html>
  1. 更适合PC端,兼容ie7+,主要功能点是window.URL.createObjectURL
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>files-pc</title>
</head>
<body>
  <input type="file" id="file" onchange="showPreview(this.id,'portrait')" />
  <img src="" id="portrait" style="width: 200px; height: 200px; display: block;" />
  <script type="text/javascript">
  /* 图片预览 */
  function showPreview(fileId, imgId) {
    var file = document.getElementById(fileId);
    var ua = navigator.userAgent.toLowerCase();
    var url = '';
    if(/msie/.test(ua)) {
      url = file.value;
    } else {
      url = window.URL.createObjectURL(file.files[0]);
    }
    document.getElementById(imgId).src = url;
  }
  </script>
</body>
</html>

vue 点击事件在UC浏览器下无效

今天早上朋友找我说遇到一个问题UC浏览器点击无效,听说折磨同事好久,我看看了,出了各种主意也没有解决,毕竟没有源代码无法根据问题判断原因所在,不过后来朋友告诉我解决了,用来一个方法 CSS3 新属性 pointer-events

一、问题

  1. 在 UC 浏览器点击事件无效(其他浏览器无问题)。
    附上朋友源码:

    <template>
            <div class="music" @click="playBtn">
                <div class="control"><-- 背景图片 --></control>
        </div>
    </template>
    <script>
        export default {
                data() {
                    return {
                        isPlay: true
                    }
                },
                methods: {
                    playBtn() {
                        if (this.isPlay) {
                            // 切换图片
                        }
                    }
                }
        }
    </script>

二、分析原因(个人理解,不喜勿喷,有错误请指出,感谢)

因为移动端部分浏览器点击图片放大,所以会在子元素默认给你绑定一个事件,就会影响你父级元素绑定的事件。
注意:子元素有背景图片或者子元素是img。
img 我添加默认事件可以理解,背景图片也影响是什么鬼,估计是检索url地址给元素绑定默认事件了。
我记得微信浏览器也可以点击放大图片,微信点击放大图片是通过微信API将图片传给微信,点击图片就可以放大了。但是 UC 这个就恶心了,你检索页面图片地址调用你自己的API,但是你绑定默认事件时别影响我正常事件啊。

三、解决方法 pointer-events

给子元素添加样式:

    .control {
        poninter-events: none;
    }

四、 pointer-events详解

简介

官方解释:

pointer-events CSS 属性指定在什么情况下 (如果有) 某个特定的图形元素可以成为鼠标事件的 target。

个人理解和使用:

pointer-event:none; 顾名思义是鼠标事件拜拜的意思,元素使用了该属性,链接啊,点击什么的都统统失效了。

pointer-events:none 的作用是让元素实体“虚化”。例如一个应用pointer-events:none的按钮元素,则我们在页面上看到的这个按钮,只是一个虚幻的影子而已,您可以理解为海市蜃楼,幽灵的躯体。当我们用手触碰它的时候可以轻易地没有任何感觉地从中穿过去。

一、简介

开发者需要知道,用户正在离开页面,常用的方法是监听下面三个事件。
- pagehide
- beforeunload
- unload

但是,这些事件在手机上可能不会触发,页面就直接关闭了,因为手机系统可以将一个进程直接转入后台,然后沙死。
- 用户点击了一条系统通知,切换到另一个 App.
- 用户进入任务切换窗口, 切换到另一个 App.
- 用户点击了 home 按钮, 切换回主屏幕。
- 操作系统自动切换到另一个 App (比如,收到一个电话)。

上面这些情况,都会导致手机进程切换到后台,然后为了节省资源,可能就会杀死浏览器进程。
以前,页面被系统切换,以及系统清除浏览器进程,是无法监听到的。页面开发者想要指定,任何一种页面卸载情况下都会执行的代码,也是无法做到的,为了解决这个问题,就诞生了 Page Visibility API 。不管手机或桌面电脑,所有情况下,这个API 都会监听到页面的可见性发生变化。

visibilityState 这个 API 的意义在于,通过监听页面的可见性,可以预判网页卸载,还可以用来节省资源,减缓电能的消耗。比如, 一但用户不看网页,下面这些网页行为都是可以暂停的。
+ 对服务器的轮询
+ 网页动画
+ 正在播放的音频或视频

二、 document.visibilityState

这个 API 主要在 document 对象上,新增了一个 docuemnt.visibilityState 属性。该属性返回一个字符串,表示页面当前的可见性状态,共有三个可能的值
+ hidden: 页面彻底不可见。
+ visible: 页面至少一部分可见。
+ prerender: 页面即将或正在渲染,处于不可见状态

其中, hidden 状态和 visible 状态是所有浏览器都必须支持的,prerender 状态只在支持“预渲染”的浏览器上才会出现,比如 Chrome 浏览器就会有预渲染功能,可以在用户不可见的状态下,预先把页面渲染出来,等到用户要浏览的时候,直接展示渲染好的网页。

只要页面可见,哪怕只露出一个角, document.visibilityState 属性就返回 visible,只有以下四种情况,才会返回 hidden.
+ 浏览器最小化
+ 浏览器没有最小化,但是当前页面切换成了背景页面。
+ 浏览器将要卸载(onload)页面
+ 操作系统触发锁屏屏幕

可以看到,上面四种场景涵盖了页面可能被卸载的所有情况。也就是说,页面卸载之前, document.visibilityState 属性一定会变成 hidden。 事实上,这也是设计这个 API 的主要目的。

另外,早期版本的 API, 这个属性还有第四个值 unloaded, 表示页面即将卸载,现在已经被废弃了。
注意, document.visibilityState 属性只针对顶层窗口,内嵌的 页面的 document.visibilityState 属性由顶层窗口决定,使用 CSS 属性隐藏 页面 (比如: display: none), 并不会影响内嵌页面的可见性。

三、docuemnt.hidden

由于历史原因, 这个 API 还定义了 document.hidden 属性。 该属性只读,返回一个布尔值,表示单签页面是否可见。
当 document.visibilityState 属性返回 visible 时, document.hidden 属性返回 false; 其他情况下, 都返回 true。 该属性只是出于历史原因而保留的,只要有可能,都应该使用 document,visibilityState 属性, 而不是使用这个属性。

四、 visibilitychange事件

只要 document.visibilityState 属性发生变化,就会触发 visibilitychange 事件。 因此,可以通过监听这个事件 (通过 document.addEventListener()方法或document.onvisibilitychange 属性),跟踪页面可见性的变化。

document.addEventListener('visibilitychange', function () {
  // 用户离开了当前页面
  if (document.visibilityState === 'hidden') {
    document.title = '页面不可见';
  }

  // 用户打开或回到页面
  if (document.visibilityState === 'visible') {
    document.title = '页面可见';
  }
});

上面代码是 Page Visibility API 的最基本用法,可以监听可见性变化。

下面是另一个例子,一旦页面不可见,就暂停视频播放。

var vidElem = document.getElementById('video-demo');
document.addEventListener('visibilitychange', startStopVideo);

function startStopVideo() {
  if (document.visibilityState === 'hidden') {
    vidElem.pause();
  } else if (document.visibilityState === 'visible') {
    vidElem.play();
  }
}

五、页面卸载

下面专门讨论一下,如何正确监听页面卸载。
页面卸载可以分为三种情况。
+ 页面可见时,用户关闭 Tab 页或浏览器窗口。
+ 页面可见时,用户在当前窗口前往另一个页面。
+ 页面不可见是,用户或系统关闭浏览器窗口。

这三种情况,都会触发 visibilitychange事件。前两种情况,该事件在用户离开页面是触发; 最后一种情况,该事件在页面从可见状态变为不可见状态时触发。

由此可见,visibilitychange事件比pagehide、beforeunload、unload事件更可靠,所有情况下都会触发(从visible变为hidden)。因此,可以只监听这个事件,运行页面卸载时需要运行的代码,不用监听后面那三个事件。

甚至可以这样说,unload事件在任何情况下都不必监听,beforeunload事件只有一种适用场景,就是用户修改了表单,没有提交就离开当前页面。另一方面,指定了这两个事件的监听函数,浏览器就不会缓存当前页面。

最近遇到一个头疼的问题,跨域!!!

跨域9种解决方案

什么是跨域

说起跨域,就要知道什么是浏览器同源策略

浏览器同源策略:必须是 协议、域名、端口完全一致的 才符合同源策略

如果以上三项,有一项不同都涉及到跨域问题


为什么浏览器要设置同源策略呢?

没有同源策略限制的两大危险场景

浏览器是从两个方面去做这个同源策略的,一是针对接口的请求,二是针对Dom的查询。试想一下没有这样的限制上述两种动作有什么危险。

没有同源策略限制的接口请求

有一个小小的东西叫cookie大家应该知道,一般用来处理登录等场景,目的是让服务端知道谁发出的这次请求。如果你请求了接口进行登录,服务端验证通过后会在响应头加入Set-Cookie字段,然后下次再发请求的时候,浏览器会自动将cookie附加在HTTP请求的头字段Cookie中,服务端就能知道这个用户已经登录过了。知道这个之后,我们来看场景:
1.你准备去清空你的购物车,于是打开了买买买网站www.maimaimai.com,然后登录成功,一看,购物车东西这么少,不行,还得买多点。
2.你在看有什么东西买的过程中,你的好基友发给你一个链接www.nidongde.com,一脸yin笑地跟你说:“你懂的”,你毫不犹豫打开了。
3.你饶有兴致地浏览着www.nidongde.com,谁知这个网站暗地里做了些不可描述的事情!由于没有同源策略的限制,它向www.maimaimai.com发起了请求!聪明的你一定想到上面的话“服务端验证通过后会在响应头加入Set-Cookie字段,然后下次再发请求的时候,浏览器会自动将cookie附加在HTTP请求的头字段Cookie中”,这样一来,这个不法网站就相当于登录了你的账号,可以为所欲为了!如果这不是一个买买买账号,而是你的银行账号,那……
这就是传说中的CSRF攻击浅谈CSRF攻击方式
看了这波CSRF攻击我在想,即使有了同源策略限制,但cookie是明文的,还不是一样能拿下来。于是我看了一些cookie相关的文章聊一聊 cookieCookie/Session的机制与安全,知道了服务端可以设置httpOnly,使得前端无法操作cookie,如果没有这样的设置,像XSS攻击就可以去获取到cookieWeb安全测试之XSS;设置secure,则保证在https的加密通信中传输以防截获。

没有同源策略限制的Dom查询

1.有一天你刚睡醒,收到一封邮件,说是你的银行账号有风险,赶紧点进www.yinghang.com改密码。你吓尿了,赶紧点进去,还是熟悉的银行登录界面,你果断输入你的账号密码,登录进去看看钱有没有少了。
2.睡眼朦胧的你没看清楚,平时访问的银行网站是www.yinhang.com,而现在访问的是www.yinghang.com,这个钓鱼网站做了什么呢?

// HTML
<iframe name="yinhang" src="www.yinhang.com"></iframe>
// JS
// 由于没有同源策略的限制,钓鱼网站可以直接拿到别的网站的Dom
const iframe = window.frames['yinhang']
const node = iframe.document.getElementById('你输入账号密码的Input')
console.log(`拿到了这个${node},我还拿不到你刚刚输入的账号密码吗`)

由此我们知道,同源策略确实能规避一些危险,不是说有了同源策略就安全,只是说同源策略是一种浏览器最基本的安全机制,毕竟能提高一点攻击的成本。其实没有刺不穿的盾,只是攻击的成本和攻击成功后获得的利益成不成正比。

跨域解决方案

1、 通过jsonp跨域
2、 document.domain + iframe跨域
3、 location.hash + iframe
4、 window.name + iframe跨域
5、 postMessage跨域
6、 跨域资源共享(CORS)
7、 nginx代理跨域
8、 nodejs中间件代理跨域
9、 WebSocket协议跨域

跨域9种方式

1. jsonp :最常见的 jsonp 方法,利用浏览器请求静态资源

node 代码

const express = require("express");
const app = express();

app.get("/push", function (req, res) {
    console.log(req.query);
    res.send(`${req.query.callback}(${JSON.stringify({ data: req.query, status: 200 })})`);
});

app.listen(3001, () => {
    console.log("jsonp 跨域");
}); 

简单版前端

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
  </head>
  <body>
    <script type='text/javascript'>
      // 后端返回直接执行的方法,相当于执行这个方法,由于后端把返回的数据放在方法的参数里,所以这里能拿到res。
      window.jsonpCb = function (res) {
        console.log(res)
      }
    </script>
    <script src='http://localhost:9871/api/jsonp?msg=helloJsonp&cb=jsonpCb' type='text/javascript'></script>
  </body>
</html>

简单封装一下前端这个套路

function jsonp(obj) {
    return new Promise((resolve, reject) => {
        const script = document.createElement('script');
        let dataString = obj.url.indexOf('?') == -1 ? '?' : '&';
        for (let i in obj.data) {
            dataString += i + "=" + obj.data[i] + "&";
        }

        const jsonp = 'json_cd' + (Math.random().toString().replace('.', ''));
        script.src = obj.url + dataString + 'callback=' + jsonp;
        document.body.appendChild(script);
        window[jsonp] = (data) => {
            document.body.removeChild(script);
            resolve(data);
        }
    })
}

jsonp({
    url: 'http://localhost:3001/push',
    data: {
        name: 'dz',
        age: '26'
    }
}).then((data) => {
    console.log(data);
})

jsonp 缺点是只能发送 get 请求


2. document.domain + iframe 跨域

此方案仅限主域相同,子域不同的跨域应用场景

实现原理: 两个页面都通过 js 强制设置 document.domain 为基础主域,就实现了同域。

a. 父窗口: (http://www.domain.com/a.html)

<iframe id="iframe" src="http://child.domain.com/b.html"></iframe>
<script>
    document.domain = 'domain.com';
    var user = 'admin';
</script>

b. 子窗口: http://child.domain.com/b.html

<script>
    document.domain = 'domain.com';
    // 获取父窗口中变量
    alert('get js data from parent ---> ' + window.parent.user);
</script>

3. location.hash + iframe 跨域

实现原理: a 与 b 跨域相互同行,通过中间页面 c 来实现。 三个页面,不同域之间利用 iframe 的 location.hash 传值,相同域之间直接 js 访问来通信。

具体实现: A域:a.html -> B域:b.html -> A域: c.html。a 与 b 不同域只能通过 hash 值单向通信,b 与 c 也不同域也只能单向通信, 但 c 与 a 同域,所以 c 可以通过 parent.parent 访问 a 页面所有的对象。

a.html:  (http://www.domain1.com/a.html)

<iframe id="iframe" src="http://www.domain2.com/b.html" style="display:none;"></iframe>
<script>
    var iframe = document.getElementById('iframe');

    // 向b.html传hash值
    setTimeout(function() {
        iframe.src = iframe.src + '#user=admin';
    }, 1000);

    // 开放给同域c.html的回调方法
    function onCallback(res) {
        alert('data from c.html ---> ' + res);
    }
</script>

b.html: (http://www.domain2.com/b.html)

<iframe id="iframe" src="http://www.domain1.com/c.html" style="display:none;"></iframe>
<script>
    var iframe = document.getElementById('iframe');

    // 监听a.html传来的hash值,再传给c.html
    window.onhashchange = function () {
        iframe.src = iframe.src + location.hash;
    };
</script>

c.html: (http://www.domain1.com/c.html)

<script>
    // 监听b.html传来的hash值
    window.onhashchange = function () {
        // 再通过操作同域a.html的js回调,将结果传回
        window.parent.parent.onCallback('hello: ' + location.hash.replace('#user=', ''));
    };
</script>

4. window.name + iframe 跨域

window.name 属性的独特之处: name 值在不同页面(甚至不同域名)加载后依旧存在,并且可以支持非常长的 name 值(2MB)。

a.html: (http://www.domain1.com/a.html)

var proxy = function(url, callback) {
    var state = 0;
    var iframe = document.createElement('iframe');

    // 加载跨域页面
    iframe.src = url;

    // onload事件会触发2次,第1次加载跨域页,并留存数据于window.name
    iframe.onload = function() {
        if (state === 1) {
            // 第2次onload(同域proxy页)成功后,读取同域window.name中数据
            callback(iframe.contentWindow.name);
            destoryFrame();

        } else if (state === 0) {
            // 第1次onload(跨域页)成功后,切换到同域代理页面
            iframe.contentWindow.location = 'http://www.domain1.com/proxy.html';
            state = 1;
        }
    };

    document.body.appendChild(iframe);

    // 获取数据以后销毁这个iframe,释放内存;这也保证了安全(不被其他域frame js访问)
    function destoryFrame() {
        iframe.contentWindow.document.write('');
        iframe.contentWindow.close();
        document.body.removeChild(iframe);
    }
};

// 请求跨域b页面数据
proxy('http://www.domain2.com/b.html', function(data){
    alert(data);
});

proxy.html: (http://www.domain1.com/proxy.html)

中间代理页,与 a.html 同域,内容为空即可。

b.html: (http;//www.domain2.com/b.html)

<script>
    window.name = 'This is domain2 data!';
</script>

总结: 通过 iframe 的 src 属性由外域向本地域,跨域数据即由 iframe 的 window.name 从外域传递到本地域。这个就巧妙的绕过了浏览器的跨域限制,但同时它又是安全操作。

5. postMessage 跨域

postMessage 是HTML5 XMLHttpRequest Level 2 中的 API,而且是为数不多可以跨域操作的 window 属性之一, 它可用于解决以下方面的问题;

a.) 页面和其打开的新窗口的数据传递
b.) 多窗口之间消息传递
c.) 页面与嵌套的 iframe 消息传递
d.) 上面三个场景的跨域数据传递

用法: postmessage(data, origin) 方法接受俩个参数
data : html5 规范支持任意基本类型或可复制的兑现,但部分浏览器只支持字符串,所以传参时最好用 JSON.string() 序列化。
origin: 协议 + 主机 + 端口号, 也可以设置为 "*",  表示可以传递给任意窗口,如果要指定和当前窗口同源的话设置为"/"。

1.)a.html:(http://www.domain1.com/a.html)

<iframe id="iframe" src="http://www.domain2.com/b.html" style="display:none;"></iframe>
<script>       
    var iframe = document.getElementById('iframe');
    iframe.onload = function() {
        var data = {
            name: 'aym'
        };
        // 向domain2传送跨域数据
        iframe.contentWindow.postMessage(JSON.stringify(data), 'http://www.domain2.com');
    };

    // 接受domain2返回数据
    window.addEventListener('message', function(e) {
        alert('data from domain2 ---> ' + e.data);
    }, false);
</script>

2.)b.html:(http://www.domain2.com/b.html)

<script>
    // 接收domain1的数据
    window.addEventListener('message', function(e) {
        alert('data from domain1 ---> ' + e.data);

        var data = JSON.parse(e.data);
        if (data) {
            data.number = 16;

            // 处理后再发回domain1
            window.parent.postMessage(JSON.stringify(data), 'http://www.domain1.com');
        }
    }, false);
</script>

6.跨域资源共享(CORS)

普通跨域请求:只服务端设置Access-Control-Allow-Origin即可,前端无须设置,若要带cookie请求:前后端都需要设置。

需注意的是:由于同源策略的限制,所读取的cookie为跨域请求接口所在域的cookie,而非当前页。如果想实现当前页cookie的写入,可参考下文:七、nginx反向代理中设置proxy_cookie_domain 和 八、NodeJs中间件代理中cookieDomainRewrite参数的设置。

目前,所有浏览器都支持该功能(IE8+:IE8/9需要使用XDomainRequest对象来支持CORS)),CORS也已经成为主流的跨域解决方案。

1.  前端设置

1.)原生ajax

// 前端设置是否带cookie
xhr.withCredentials = true;

示例代码:

var xhr = new XMLHttpRequest(); // IE8/9需用window.XDomainRequest兼容

// 前端设置是否带cookie
xhr.withCredentials = true;

xhr.open('post', 'http://www.domain2.com:8080/login', true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.send('user=admin');

xhr.onreadystatechange = function() {
    if (xhr.readyState == 4 && xhr.status == 200) {
        alert(xhr.responseText);
    }
};

2.)jQuery ajax

$.ajax({
    ...
   xhrFields: {
       withCredentials: true    // 前端设置是否带cookie
   },
   crossDomain: true,   // 会让请求头中包含跨域的额外信息,但不会含cookie
    ...
});

3.)vue框架

a.) axios设置:

axios.defaults.withCredentials = true

b.) vue-resource设置:

Vue.http.options.credentials = true
2、 服务端设置:

若后端设置成功,前端浏览器控制台则不会出现跨域报错信息,反之,说明没设成功。

1.)Java后台:

/*
 * 导入包:import javax.servlet.http.HttpServletResponse;
 * 接口参数中定义:HttpServletResponse response
 */

// 允许跨域访问的域名:若有端口需写全(协议+域名+端口),若没有端口末尾不用加'/'
response.setHeader("Access-Control-Allow-Origin", "http://www.domain1.com"); 

// 允许前端带认证cookie:启用此项后,上面的域名不能为'*',必须指定具体的域名,否则浏览器会提示
response.setHeader("Access-Control-Allow-Credentials", "true"); 

// 提示OPTIONS预检时,后端需要设置的两个常用自定义头
response.setHeader("Access-Control-Allow-Headers", "Content-Type,X-Requested-With");

2.)Nodejs后台示例:

var http = require('http');
var server = http.createServer();
var qs = require('querystring');

server.on('request', function(req, res) {
    var postData = '';

    // 数据块接收中
    req.addListener('data', function(chunk) {
        postData += chunk;
    });

    // 数据接收完毕
    req.addListener('end', function() {
        postData = qs.parse(postData);

        // 跨域后台设置
        res.writeHead(200, {
            'Access-Control-Allow-Credentials': 'true',     // 后端允许发送Cookie
            'Access-Control-Allow-Origin': 'http://www.domain1.com',    // 允许访问的域(协议+域名+端口)
            /* 
             * 此处设置的cookie还是domain2的而非domain1,因为后端也不能跨域写cookie(nginx反向代理可以实现),
             * 但只要domain2中写入一次cookie认证,后面的跨域接口都能从domain2中获取cookie,从而实现所有的接口都能跨域访问
             */
            'Set-Cookie': 'l=a123456;Path=/;Domain=www.domain2.com;HttpOnly'  // HttpOnly的作用是让js无法读取cookie
        });

        res.write(JSON.stringify(postData));
        res.end();
    });
});

server.listen('8080');
console.log('Server is running at port 8080...');

7. nginx代理跨域

nginx配置解决iconfont跨域

浏览器跨域访问js、css、img等常规静态资源被同源策略许可,但iconfont字体文件(eot|otf|ttf|woff|svg)例外,此时可在nginx的静态资源服务器中加入以下配置。

location / {
  add_header Access-Control-Allow-Origin *;
}
2、 nginx反向代理接口跨域

跨域原理: 同源策略是浏览器的安全策略,不是HTTP协议的一部分。服务器端调用HTTP接口只是使用HTTP协议,不会执行JS脚本,不需要同源策略,也就不存在跨越问题。

实现思路:通过nginx配置一个代理服务器(域名与domain1相同,端口不同)做跳板机,反向代理访问domain2接口,并且可以顺便修改cookie中domain信息,方便当前域cookie写入,实现跨域登录。

nginx具体配置:
#proxy服务器
server {
    listen       81;
    server_name  www.domain1.com;

    location / {
        proxy_pass   http://www.domain2.com:8080;  #反向代理
        proxy_cookie_domain www.domain2.com www.domain1.com; #修改cookie里域名
        index  index.html index.htm;

        # 当用webpack-dev-server等中间件代理接口访问nignx时,此时无浏览器参与,故没有同源限制,下面的跨域配置可不启用
        add_header Access-Control-Allow-Origin http://www.domain1.com;  #当前端只跨域不带cookie时,可为*
        add_header Access-Control-Allow-Credentials true;
    }
}

1.) 前端代码示例:

var xhr = new XMLHttpRequest();

// 前端开关:浏览器是否读写cookie
xhr.withCredentials = true;

// 访问nginx中的代理服务器
xhr.open('get', 'http://www.domain1.com:81/?user=admin', true);
xhr.send();

2.) Nodejs后台示例:

var http = require('http');
var server = http.createServer();
var qs = require('querystring');

server.on('request', function(req, res) {
    var params = qs.parse(req.url.substring(2));

    // 向前台写cookie
    res.writeHead(200, {
        'Set-Cookie': 'l=a123456;Path=/;Domain=www.domain2.com;HttpOnly'   // HttpOnly:脚本无法读取
    });

    res.write(JSON.stringify(params));
    res.end();
});

server.listen('8080');
console.log('Server is running at port 8080...');

8. Nodejs中间件代理跨域

node中间件实现跨域代理,原理大致与nginx相同,都是通过启一个代理服务器,实现数据的转发,也可以通过设置cookieDomainRewrite参数修改响应头中cookie中域名,实现当前域的cookie写入,方便接口登录认证。

1、 非vue框架的跨域(2次跨域)

利用node + express + http-proxy-middleware搭建一个proxy服务器。

1.)前端代码示例:

var xhr = new XMLHttpRequest();

// 前端开关:浏览器是否读写cookie
xhr.withCredentials = true;

// 访问http-proxy-middleware代理服务器
xhr.open('get', 'http://www.domain1.com:3000/login?user=admin', true);
xhr.send();

2.)中间件服务器:

var express = require('express');
var proxy = require('http-proxy-middleware');
var app = express();

app.use('/', proxy({
    // 代理跨域目标接口
    target: 'http://www.domain2.com:8080',
    changeOrigin: true,

    // 修改响应头信息,实现跨域并允许带cookie
    onProxyRes: function(proxyRes, req, res) {
        res.header('Access-Control-Allow-Origin', 'http://www.domain1.com');
        res.header('Access-Control-Allow-Credentials', 'true');
    },

    // 修改响应信息中的cookie域名
    cookieDomainRewrite: 'www.domain1.com'  // 可以为false,表示不修改
}));

app.listen(3000);
console.log('Proxy server is listen at port 3000...');

3.)Nodejs后台同(六:nginx)

2、 vue框架的跨域(1次跨域)

利用node + webpack + webpack-dev-server代理接口跨域。在开发环境下,由于vue渲染服务和接口代理服务都是webpack-dev-server同一个,所以页面与代理接口之间不再跨域,无须设置headers跨域信息了。

webpack.config.js部分配置:

module.exports = {
    entry: {},
    module: {},
    ...
    devServer: {
        historyApiFallback: true,
        proxy: [{
            context: '/login',
            target: 'http://www.domain2.com:8080',  // 代理跨域目标接口
            changeOrigin: true,
            secure: false,  // 当代理某些https服务报错时用
            cookieDomainRewrite: 'www.domain1.com'  // 可以为false,表示不修改
        }],
        noInfo: true
    }
}

9. WebSocket协议跨域

WebSocket protocol是HTML5一种新的协议。它实现了浏览器与服务器全双工通信,同时允许跨域通讯,是server push技术的一种很好的实现。
原生WebSocket API使用起来不太方便,我们使用Socket.io,它很好地封装了webSocket接口,提供了更简单、灵活的接口,也对不支持webSocket的浏览器提供了向下兼容。

1.)前端代码:

<div>user input:<input type="text"></div>
<script src="./socket.io.js"></script>
<script>
var socket = io('http://www.domain2.com:8080');

// 连接成功处理
socket.on('connect', function() {
    // 监听服务端消息
    socket.on('message', function(msg) {
        console.log('data from server: ---> ' + msg); 
    });

    // 监听服务端关闭
    socket.on('disconnect', function() { 
        console.log('Server socket has closed.'); 
    });
});

document.getElementsByTagName('input')[0].onblur = function() {
    socket.send(this.value);
};
</script>

2.)Nodejs socket后台:

var http = require('http');
var socket = require('socket.io');

// 启http服务
var server = http.createServer(function(req, res) {
    res.writeHead(200, {
        'Content-type': 'text/html'
    });
    res.end();
});

server.listen('8080');
console.log('Server is running at port 8080...');

// 监听socket连接
socket.listen(server).on('connection', function(client) {
    // 接收信息
    client.on('message', function(msg) {
        client.send('hello:' + msg);
        console.log('data from client: ---> ' + msg);
    });

    // 断开处理
    client.on('disconnect', function() {
        console.log('Client socket has closed.'); 
    });
});

1. 创建合并分支

1. 分支介绍

    git 每次提交都是一条时间线,称之为分支,在 git 这个分支为主分支 master 分支。

     HEAD 严格来说并不是指向提交,而是指向 master ,master 才是指向提交。所以 HEAD 指向当前分支。

2. 分支提交   

    当 git 创建分支,例如 dev 事,新建一个 dev 的指针,改 HEAD 指向 ,工作区的文件没有任何变化,不过从现在开始, 对工作区的修改和提交就针对 dev 分支了,如新的一次提交 dev 指针向前移步, 而 master 指针不变。

    之后我们就可以,合并分支删除 dev 

3. 代码练习

// 创建 dev 分支, 然后切换到 dev 分支;

    git checkout -b dev

// git checkout 命令加上 -b 参数表示创建并切换,相当于一下两条命令

    git branch dev

    git checkout dev

// 然后使用 git branch 查看分支,当前分支前面会有一个 * 号。

    git branch 

// 切换回 master 分支

    git checkout master

// 删除分支

    git branch -d dev

4. 冲突处理

// 合并分支

    git merge featurel

    $CONFLICT (content): Merge conflict in readme.txt
    $Automatic merge failed; fix conflicts and then commit the result.

告诉我们 readme.txt 文件发生冲突,必须手动解决冲突

// git status 也可以告诉我们冲突的文件

    git status 

$On branch master
$Your branch is ahead of 'origin/master' by 2 commits.
  (use "git push" to publish your local commits)

$You have unmerged paths.
  (fix conflicts and run "git commit")
  (use "git merge --abort" to abort the merge)

$Unmerged paths:
  (use "git add <file>..." to mark resolution)

    both modified:   readme.txt

$no changes added to commit (use "git add" and/or "git commit -a")

我们可以直接查看 readme.txt 内容

git 用 <<<<<<< ======= >>>>>>> 标记出不同分支内容,修改后保存

再提交:

    git add .

    git commit -m"修改文件"

// 用 git log 查看合并情况情况

    git log -graph -pretty=oneline -abbre-commit 

// 最后删除分支

    git branch -d featurel

    

       

按照公司要求,需要一个生成短信截图的程序,但是有些傻眼,是不是安排错了,作为一个有梦想前端当然不能为这点事情而难住。

程序应用自己不会写,重新学习成本太高,只能用 html 来写了。

在网上找了资料,发现 canvas  有个一 canvas.toDataURL("image/png")可以转换成 base64 编码,哈哈,这不就一半完成了!!!最大的难题已经解决。

市场上的一些js库,如:html2canvasdom-to-image,其本质也是通过toDataURL来转换成图片。

之后就是绘制 canvas 这里用到的一些简单的方法,然后用谷歌插件批量下载图片就搞定了!

【DOM 优化】

1. 缓存 DOM

const div = document.getElementById("div");

由于查询 DOM 比较耗时,在同一个节点无需多次查询的情况下,可以缓存 DOM

2. 减少 DOM 深度及 DOM 数量

HTML 中标签越多,标签的层级越深,浏览器解析 DOM 并绘制到浏览器中所花的时间就越长,所以尽可能减少 DOM 深度及 DOM 数量

3. 批量操作 DOM 

由于批量操作 DOM 比较耗时,且可能造成回流,因此尽量避免批量操作 DOM;如遇到批量操作 DOM 可以先用字符串拼接,再用 innerHTMl 更新

4. 批量操作 CSS 样式

通过切换 class 或者使用元素的 style.csstext 属性去批量操作元素的样式

5. 在内存中操作 DOM 

使用 DocumnetFragment 对象,让 DOM 操作发生在内存中,而不是页面里

6. DOM 元素离线更新

对 DOM 进行相关操作时,例、appendChild 等都可以使用 DocumentFragment 对象进行离线操作,带元素“组装”完成后再一次插入页面,或者使用 display: none 对元素隐藏,在元素“消失”后进行相关操作

7. DOM读写分离

浏览器具有惰性渲染机制,连续多次修改 DOM 可能只触发一次渲染。而如果修改 DOM 后,立即读取 DOM 。为了保证读取到的正确 DOM 值,会触发浏览器一次渲染。因此修改 DOM 操作要与访问 DOM 分开进行

8. 事件代理

事件代理是指将事件监听器注册在父级元素上,由于子元素的事件会通过事件冒泡的方式向上传播到父节点,因此可以由父节点的监听函数统一处理多个子元素的事件。

利用事件代理,可以减少内存使用,提高性能及降低代码复杂度。

9. 防抖和节流

使用函数节流(throttle)或函数去抖(debunce)限制某一个方法的频繁触发

10. 及时清理环境

及时消除对象引用,清除定时器,清除事件监听器,创建最小作用域变量,可以及时回收内存

减少重绘回流

【样式设置】

    1. 避免使用层级较深的选择器,或者其他一些复杂的选择器,以提高 CSS 渲染效率;

    2. 避免使用 CSS 表达式, CSS 表达式是动态设置 CSS 的属性,虽然强大方便,但是属于危险方法,它的问题就是在于计算频率很快。不仅仅是在页面显示和缩放时,就是在页面滚动,乃至移动鼠标时都会要重新计算一次;

    3. 元素适当地定义高度或者最小高度,否则元素的动态内容载入时,会出现页面元素的晃动或位置抖动,造成回流;

    4. 给图片设置尺寸, 如果图片不设置尺寸,首次载入时,占据空间从 0 到完全出现,上下左右都可能位移,发生回流;

    5. 不用使用 table 布局,因为一个小改动可能造成整个 table 重新布局,而且 table 渲染通常要 3 倍于同等元素时间

    6. 能够使用 CSS 实现的效果,尽量使用 CSS 而不使用 JS 实现;

【渲染层】

    1. 将需要多次重绘的元素独立为 render layer 渲染层, 如设置 absolute, 可以减少重绘范围;

    2. 对于一些进行动画的元素, 使用硬件渲染,从而避免重绘和回流;