如何在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. 对于一些进行动画的元素, 使用硬件渲染,从而避免重绘和回流; 

一、 简介

1. websocket 推送技术在 2008 诞生,2011 成为标准,现在所有浏览器都支持了。

2. 最大特点 是真正的双向平等对话,属于服务器推送技术的一种

扩展:服务器推送技术 

1. webpush 

Internet工程任务组的Webpush提议是一种使用HTTP版本2的简单协议,用于提供可以及时传递(或“推送”)的实时事件,例如传入呼叫或消息。该协议将所有实时事件合并到一个会话中,以确保更有效地使用网络和无线电资源。单个服务整合所有事件,在应用程序到达时将这些事件分发给应用程序。这只需要一个会话,避免重复的开销成本。

2. HTTP 服务器推送

HTTP服务器推送(也称为HTTP流)是一种用于将未经请求的(异步)数据从Web服务器发送到Web浏览器的机制。HTTP服务器推送可以通过几种机制中的任何一种来实现。

作为HTML5的一部分,WebSocket API允许Web服务器和客户端通过全双工 TCP连接进行通信。

3. pushlet 

在这种技术中,服务器利用持久的HTTP连接,使响应永久“打开”(即,服务器永远不会终止响应),有效地欺骗浏览器在初始页面加载被考虑之后保持“加载”模式完成。然后,服务器定期发送JavaScript片段以更新页面内容,从而实现推送功能。通过使用这种技术,客户端不需要Java applet或其他插件来保持与服务器的开放连接; 客户端会自动通知服务器推送的新事件。[11] [12]然而,这种方法的一个严重缺点是服务器缺乏对浏览器超时的控制; 如果浏览器端发生超时,则始终需要页面刷新。

4. 长轮询

5. Flash XMLSockt 中继

Cbox和其他聊天应用程序使用的这种技术在单像素Adobe Flash电影中使用了XMLSocket对象。在控制的JavaScript,客户端建立一个TCP连接到一个单向的服务器上的继电器。中继服务器不从该套接字读取任何内容; 相反,它立即向客户端发送唯一标识符。接下来,客户端发出HTTP请求到web服务器,包括它的这个标识符。然后,Web应用程序可以将发往客户端的消息推送到中继服务器的本地接口,中继服务器通过Flash套接字对它们进行中继。这种方法的优点在于它欣赏许多Web应用程序(包括聊天)的典型自然读写不对称性,因此它提供了高效率。因为它不接受即将离任的插座数据,中继服务器不需要轮询传出TCP连接,在所有的,从而能够容纳的并发连接打开数万人。在此模型中,规模限制是底层服务器操作系统的TCP堆栈。

6. 可靠的集团数据交付(RGDD)

在云计算等服务中,为了提高数据的可靠性和可用性,通常会将其推送(复制)到多台计算机上。例如,Hadoop分布式文件系统(HDFS)为存储的任何对象生成2个额外副本。RGDD专注于有效地将对象从一个位置转换为多个位置,同时通过在网络上的任何链接上发送对象的最小数量的副本(在最佳情况下只有一个)来节省带宽。例如,Datacast [15]是一种向数据中心内的许多节点传递的方案,它依赖于常规和结构化拓扑,DCCast [16]是一种跨数据中心交付的类似方法。

7. 推送通知

推送通知是从后端服务器或应用程序“推送”到用户界面的消息,例如(但不限于)移动应用程序和桌面应用程序。Apple于2009年首次推出推送通知。[17] 2010年,Google发布了自己的服务,即Google Cloud to Device Messaging。(此后它已被Google云消息传递Firebase云消息传递所取代。)[18] 2015年11月,微软宣布推出Windows通知服务将扩展为使用通用Windows平台架构,允许使用通用API调用和POST请求将推送数据发送到Windows 10,Windows 10移动版,Xbox以及其他支持的平台。[19]

特点包括:

(1)建立在 TCP 协议之上,服务器端的实现比较容易。

(2)与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。

(3)数据格式比较轻量,性能开销小,通信高效。

(4)可以发送文本,也可以发送二进制数据。

(5)没有同源限制,客户端可以与任意服务器通信。

(6)协议标识符是ws(如果加密,则为wss),服务器网址就是 URL。

ws://example.com:80/some/path


二、客户端示例

var ws = new WebSocket("wss://echo.websocket.org");

ws.onopen = function(evt) { 
  console.log("Connection open ..."); 
  ws.send("Hello WebSockets!");
};

ws.onmessage = function(evt) {
  console.log( "Received Message: " + evt.data);
  ws.close();
};

ws.onclose = function(evt) {
  console.log("Connection closed.");
};    


三、客户端 API

4.1 构造函数

var ws = new WebSocket('ws://localhost:8080');

4.2 webSocket.readyState

readyState 属性返回实例对象的当前状态,共有四种

CONNECTING:值为0,表示正在连接。
OPEN:值为1,表示连接成功,可以通信了。
CLOSING:值为2,表示连接正在关闭。
CLOSED:值为3,表示连接已经关闭,或者打开连接失败。

示例

switch (ws.readyState) {
  case WebSocket.CONNECTING:
    // do something
    break;
  case WebSocket.OPEN:
    // do something
    break;
  case WebSocket.CLOSING:
    // do something
    break;
  case WebSocket.CLOSED:
    // do something
    break;
  default:
    // this never happens
    break;
}

4.3 webSocket.onopen

实例对象的 onopen 属性,用于指定连接成功后的回调函数

ws.onopen = function () {
  ws.send('Hello Server!');
}

如果指定多个回调函数,可以使用 addEventListnenr 方法

ws.addEventListener('open', function (event) {
  ws.send('Hello Server!');
});

4.4 webSocket.onclose 

实例对象的 onclose 属性,用于指定连接关闭后的回调函数

ws.onclose = function(event) {
  var code = event.code;
  var reason = event.reason;
  var wasClean = event.wasClean;
  // handle close event
};

ws.addEventListener("close", function(event) {
  var code = event.code;
  var reason = event.reason;
  var wasClean = event.wasClean;
  // handle close event
});

4.5 webSocket.onmessage

 实例对象的 onmessage 属性,用于指定收到服务器数据后的回调函数

ws.onmessage = function(event) {
  var data = event.data;
  // 处理数据
};

ws.addEventListener("message", function(event) {
  var data = event.data;
  // 处理数据
});

注意,服务器数据可能是文本,也可能是二进制数据(blob 对象或 Arraybuffer 对象)

ws.onmessage = function(event){
  if(typeof event.data === String) {
    console.log("Received data string");
  }

  if(event.data instanceof ArrayBuffer){
    var buffer = event.data;
    console.log("Received arraybuffer");
  }
}

出来动态判断收到的数据类型,也可以使用 binaryType 属性,显式指定收到的二进制数据类

// Sending canvas ImageData as ArrayBuffer
var img = canvas_context.getImageData(0, 0, 400, 320);
var binary = new Uint8Array(img.data.length);
for (var i = 0; i < img.data.length; i++) {
  binary[i] = img.data[i];
}
ws.send(binary.buffer);

型。

// 收到的是 blob 数据
ws.binaryType = "blob";
ws.onmessage = function(e) {
  console.log(e.data.size);
};

// 收到的是 ArrayBuffer 数据
ws.binaryType = "arraybuffer";
ws.onmessage = function(e) {
  console.log(e.data.byteLength);
};

4.6 webSocket.sed()

实例对象的 send() 方法用于向服务器发送数据

发送文本的例子

ws.send('your message');

发送 Blob 对象的例子

var file = document
  .querySelector('input[type="file"]')
  .files[0];
ws.send(file);

发送 ArrayBuffer 对象的例子

// Sending canvas ImageData as ArrayBuffer
var img = canvas_context.getImageData(0, 0, 400, 320);
var binary = new Uint8Array(img.data.length);
for (var i = 0; i < img.data.length; i++) {
  binary[i] = img.data[i];
}
ws.send(binary.buffer);

4.7 webSocket.bufferedAmount

实例对象的bufferedAmount属性,表示还有多少字节的二进制数据没有发送出去。它可以用来判断发送是否结束。

var data = new ArrayBuffer(10000000);
socket.send(data);

if (socket.bufferedAmount === 0) {
  // 发送完毕
} else {
  // 发送还没结束
}

4.8 webSocket.onerror 

实例对象的onerror属性,用于指定报错时的回调函数。

socket.onerror = function(event) {
  // handle error event
};

socket.addEventListener("error", function(event) {
  // handle error event
});

1. oncontextmenu=”window.event.returnValue=false” 将彻底屏蔽鼠标右键 

    < table border oncontextmenu=return(false)>< td>no< /table> 可用于 Table 

2. < body onselectstart=”return false”> 取消选取、防止复制 
 
3. onpaste=”return false” 不准粘贴 
 
4. oncopy=”return false;” oncut=”return false;” 防止复制 
 
5. < link rel=”Shortcut Icon” href=”favicon.ico”> IE 地址栏前换成自己的图标 
 
6. < link rel=”Bookmark” href=”favicon.ico”> 可以在收藏夹中显示出你的图标 
 
7. < input style=”ime-mode:disabled”> 关闭输入法 
 
8. 永远都会带着框架 < script language=”JavaScript”>< !– if (window == top)top.location.href = “frames.htm”; //frames.htm 为框架网页 // –>< /script> 
 
9. 防止被人 frame < SCRIPT LANGUAGE=JAVASCRIPT>< !–  if (top.location != self.location)top.location=self.location; // –>< /SCRIPT> 
 
10. 网页将不能被另存为 

<noscript>< iframe src=*.html>< /iframe></noscript>  

11.  查看网页源代码

<input type=button value=查看网页源代码 onclick="window.location='view-source:' + window.location.href">

12.删除时确认 

 <a href="javascript:if(confirm('确实要删除吗?'))location='boos.asp?&areyou=删除&page=1'">删除</a>

13. 取得控件的绝对位置  // Javascript

    function getIE(e) {
        var t = e.offsetTop; var l = e.offsetLeft; while (e = e.offsetParent) {
            t += e.offsetTop; l += e.offsetLeft;  } alert("top=" + t + "/nleft=" + l);
    } 

//VBScript < script language=”VBScript”>< !– function getIE() dim t,l,a,b set a=document.all.img1 t=document.all.img1.offsetTop l=document.all.img1.offsetLeft while a.tagName< >”BODY” set a = a.offsetParent t=t+a.offsetTop l=l+a.offsetLeft wend msgbox “top=”&t&chr(13)&”left=”&l,64,”得到控件的位置” end function –>< /script> 
 

14. 光标是停在文本框文字的最后

    <input type=text name=text1 value=”123″ onfocus="cc()">
    <script language=”javascript”>
        function cc() {
            var e = event.srcElement;
            var r = e.createTextRange();
            r.moveStart("character", e.value.length);
            r.collapse(true);
            r.select();
        }
    </script>

15. 判断上一页的来源 

 document.referrer 

16. 最小化、最大化、关闭窗口 < object id=hh1 classid=”clsid:ADB880A6-D8FF-11CF-9377-00AA003B7A11″>  < param name=”Command” value=”Minimize”>< /object> < object id=hh2 classid=”clsid:ADB880A6-D8FF-11CF-9377-00AA003B7A11″>  < param name=”Command” value=”Maximize”>< /object> < OBJECT id=hh3 classid=”clsid:adb880a6-d8ff-11cf-9377-00aa003b7a11″> 
< PARAM NAME=”Command” VALUE=”Close”>< /OBJECT> < input type=button value=最小化 onclick=hh1.Click()> < input type=button value=最大化 onclick=hh2.Click()> < input type=button value=关闭 onclick=hh3.Click()> 本例适用于 IE 
 

17.屏蔽功能键 

 function look() {
      if (event.shiftKey) alert("禁止按 Shift 键!"); //可以换成 ALT CTRL 
 }
document.onkeydown = look;

 

18. 网页不会被缓存 

<META HTTP-EQUIV="pragma" CONTENT="no-cache"> <META HTTP-EQUIV="Cache-Control" CONTENT="no-cache, must-revalidate"> <META HTTP-EQUIV="expires" CONTENT="Wed, 26 Feb 1997 08:21:57 GMT"> 或者<META HTTP-EQUIV="expires" CONTENT="0"> 

19.怎样让表单没有凹凸感? 

<input type=text style="border:1 solid #000000">  
或 
<input type=text style="border-left:none; border-right:none; border-top:none; border-bottom: 1px solid #000000">< /textarea> 

 
20.< div>< span>&< layer>的区别?  < div>(division)用来定义大段的页面元素,会产生转行  < span>用来定义同一行内的元素,跟< div>的唯一区别是不产生转行  < layer>是 ns 的标记,ie 不支持,相当于< div> 
 

21.让弹出窗口总是在最上面: 

<body onblur="this.focus();"> 

22.不要滚动条?

//  让竖条没有:  
<body style="overflow:scroll;overflow-y:hidden">  </body> 
// 让横条没有: 
 <body style="overflow:scroll;overflow-x:hidden">  </body>  
// 两个都去掉?更简单了  
<body scroll="no">  </body>  

23.怎样去掉图片链接点击后,图片周围的虚线?

 <a href="#" onFocus="this.blur()"><img src="logo.jpg" border=0></a> 

24.电子邮件处理提交表单 

<form name="form1" method="post" action="mailto:****@***.com" enctype="text/plain">  
   <input type=submit> 
</form> 

 
25.在打开的子窗口刷新父窗口的代码里如何写? window.opener.location.reload()  
 
26.如何设定打开页面的大小 < body onload=”top.resizeTo(300,200);”> 打开页面的位置< body onload=”top.moveBy(300,200);”> 
 
27.在页面中如何加入不是满铺的背景图片,拉动页面时背景图不动  < STYLE>  body  {background-image:url(logo.gif); background-repeat:no-repeat;  background-position:center;background-attachment: fixed}  < /STYLE>  
 
28. 检查一段字符串是否全由数字组成 < script language=”Javascript”>< !– function checkNum(str){return str.match(//D/)==null} alert(checkNum(“1232142141”)) alert(checkNum(“123214214a1”)) // –>< /script> 
 
29. 获得一个窗口的大小 document.body.clientWidth; document.body.clientHeight 
 
30. 怎么判断是否是字符 if (/[^/x00-/xff]/g.test(s)) alert(“含有汉字”); else alert(“全是字符”); 
 
31.TEXTAREA 自适应文字行数的多少 < textarea rows=1 name=s1 cols=27 onpropertychange=”this.style.posHeight=this.scrollHeight”> < /textarea> 
 
32. 日期减去天数等于第二个日期 
< script language=Javascript> function cc(dd,dadd) { //可以加上错误处理 var a = new Date(dd) a = a.valueOf() a = a – dadd * 24 * 60 * 60 * 1000 a = new Date(a) alert(a.getFullYear() + “年” + (a.getMonth() + 1) + “月” + a.getDate() + “日”) } cc(“12/23/2002”,2) < /script> 
 
33. 选择了哪一个 Radio < HTML>< script language=”vbscript”> function checkme() for each ob in radio1 if ob.checked then window.alert ob.value next end function < /script>< BODY> < INPUT name=”radio1″ type=”radio” value=”style” checked>Style < INPUT name=”radio1″ type=”radio” value=”barcode”>Barcode < INPUT type=”button” value=”check” onclick=”checkme()”> < /BODY>< /HTML> 
 
34.脚本永不出错 < SCRIPT LANGUAGE=”JavaScript”>  < !– Hide  function killErrors() {  return true;  }  window.onerror = killErrors;  // –>  < /SCRIPT> 
 
35.ENTER 键可以让光标移到下一个输入框 <input onkeydown=”if(event.keyCode==13)event.keyCode=9″> 
 
36. 检测某个网站的链接速度: 把如下代码加入< body>区域中: < script language=Javascript> tim=1 setInterval(“tim++”,100) 
b=1 var autourl=new Array() autourl[1]=”www.njcatv.net” autourl[2]=”javacool.3322.net” autourl[3]=”www.sina.com.cn” autourl[4]=”www.nuaa.edu.cn” autourl[5]=”www.cctv.com” function butt(){ document.write(“< form name=autof>”) for(var i=1;i< autourl.length;i++) document.write(“< input type=text name=txt”+i+” size=10 value=测试中……> =》< input type=text  name=url”+i+” size=40> =》< input type=button value=GO  
onclick=window.open(this.form.url”+i+”.value)> “) document.write(“< input type=submit value=刷新>< /form>”) } butt() function auto(url){ document.forms[0][“url”+b].value=url if(tim>200) {document.forms[0][“txt”+b].value=”链接超时”} else {document.forms[0][“txt”+b].value=”时间”+tim/10+”秒”} b++ } function run(){for(var i=1;i< autourl.length;i++)document.write(“< img src=http://”+autourl+”/”+Math.random()+”  
 
width=1 height=1  
 
onerror=auto(“http://”+autourl+””)>”)} run()< /script> 
 
37. 各种样式的光标 auto :标准光标 default :标准箭头 hand :手形光标 wait :等待光标 text :I 形光标 vertical-text :水平 I 形光标 no-drop :不可拖动光标 not-allowed :无效光标 
help :?帮助光标 all-scroll :三角方向标 move :移动标 crosshair :十字标 e-resize n-resize nw-resize w-resize s-resize se-resize sw-resize 
 
38.页面进入和退出的特效 进入页面< meta http-equiv=”Page-Enter” content=”revealTrans(duration=x, transition=y)”> 推出页面< meta http-equiv=”Page-Exit” content=”revealTrans(duration=x, transition=y)”>   这个是页面被载入和调出时的一些特效。duration 表示特效的持续时间,以秒为单位。 transition 表示使用哪种特效,取值为 
 
1-23:   0 矩形缩小    1 矩形扩大    2 圆形缩小   3 圆形扩大    4 下到上刷新    5 上到下刷新   6 左到右刷新    7 右到左刷新    8 竖百叶窗   9 横百叶窗    10 错位横百叶窗    11 错位竖百叶窗   12 点扩散    13 左右到中间刷新    14 中间到左右刷新   15 中间到上下   16 上下到中间    17 右下到左上   18 右上到左下    19 左上到右下    20 左下到右上   21 横条    22 竖条    23 以上22 种随机选择一种 
 
39.在规定时间内跳转 < META http-equiv=V=”REFRESH” content=”5;URL=http://www.51js.com”>  
 
40.网页是否被检索 < meta name=”ROBOTS” content=”属性值”>   其中属性值有以下一些:   属性值为”all”: 文件将被检索,且页上链接可被查询;   属性值为”none”: 文件不被检索,而且不查询页上的链接;   属性值为”index”: 文件将被检索;   属性值为”follow”: 查询页上的链接;   属性值为”noindex”: 文件不检索,但可被查询链接;   属性值为”nofollow”: 文件不被检索,但可查询页上的链接。 
 
41.回车 用客户端脚本在页面添加document的onkeydown事件,让页面在接受到回车事件后,进行Tab 键的功能,即只要把 event 的 keyCode 由 13 变为 9 Javascript 代码如下: 
 
<script language=”javascript” for=”document” event=”onkeydown”> <!–   if(event.keyCode==13)      event.keyCode=9; –> </script> 这样的处理方式,可以实现焦点往下移动,但对于按钮也起同样的作用,一般的客户在输入完 资料以后,跳到按钮后,最好能直接按”回车”进行数据的提交.因此,对上面的方法要进行一下 修改,应该对于”提交”按钮不进行焦点转移.而直接激活提交. 因此我对上面的代码进行了一个修改,即判断事件的”源”,是否为提交按钮,代码如下: <script language=”javascript” for=”document” event=”onkeydown”> <!–   if(event.keyCode==13 && event.srcElement.type!=’button’ && event.srcElement.type!=’submit’ && event.srcElement.type!=’reset’ && event.srcElement.type!=’textarea’ && event.srcElement.type!=”)      event.keyCode=9; –> </script> 判断是否为 button, 是因为在 HTML 上会有 type=”button” 判断是否为 submit,是因为 HTML 上会有 type=”submit” 判断是否为reset,是因为 HTML 上的”重置”应该要被执行 判断是否为空,是因为对于 HTML 上的”<a>链接”也应该被执行,这种情况发生的情况不多,可 以使用”tabindex=-1″的方式来取消链接获得焦点. 

在 css 样式上 -webkit-box-orient:vertical;  加上注释, 

如下:

/* autoprefixer: off */
 -webkit-box-orient: vertical; 
 /* autoprefixer: on */

打包时必须使用这种方法打包,否则打包后  -webkit-box-orient: vertical 便会消失

网上解决方案是这样的,但是我在我的项目中发现不起作用,

解决方案

optimize-css-assets-webpack-plugin这个插件的问题
注释掉webpack.prod.conf.js中下面的代码

new OptimizeCSSPlugin({
  cssProcessorOptions: config.build.productionSourceMap
    ? { safe: true, map: { inline: false } }
    : { safe: true }
}),

组件传值

1. 父向子组件传值,和传递方法

父组件 app-home 

子组件 app-header

// — app-home.html 页面

<app-header [msg]="msg" [run]='run' [getDataFromChild]="getDataFromChild"></app-header>


// — app-home.ts 

import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'app-home',
  templateUrl: './app-home.html',
  styleUrls: ['./app-home.css']
})
export class HomeComponent implements OnInit {

  public msg="我是home组件的msg";

  constructor() { }

  ngOnInit() {
  }

  run(){

    alert('这是home组件的run方法');
  }

  getDataFromChild(childData){  /*父组件*/
    alert(childData+"1111");
  }

}


// — app.header.ts 页面

import { Component, OnInit,Input} from '@angular/core';

@Component({
  selector: 'app-header',
  templateUrl: './app-header.html',
  styleUrls: ['./app-header.css']
})
export class FooterComponent implements OnInit {

  @Input()  msg;


  @Input()  run;  /*接收父组件传过来的run方法*/

  @Input()  getDataFromChild; 


  public msginfo='这是子组件的数据';

  constructor() { }

  ngOnInit() {
  }

  sendParent(){  /*子组件自己的方法*/
      this.getDataFromChild(this.msginfo);  /*子组件调用父组件的方法*/

  }

}

1.2 子组件用广播方式调用父组件方法

// — app-home.html

<app-footer [msg]="msg" [run]='run' (toparent)="getDataFromChild"></app-footer>

// — app-home.ts 

import { Component, OnInit } from '@angular/core';
import {Http,Jsonp} from "@angular/http";
@Component({
  selector: 'app-news',
  templateUrl: './app-home.html',
  styleUrls: ['./app-home.css']
})
export class NewsComponent implements OnInit {
   public msg="msg";
   public name="张三";

   public list=[];
  constructor(private http:Http,private jsonp:Jsonp) { }

  ngOnInit() {
  }

  getDataFromChild(childData){  /*父组件*/
    alert(childData+"1111");
  }
}

//— app-header.ts

import { Component, OnInit,Input,Output,EventEmitter } from '@angular/core';

@Component({
  selector: 'app-header',
  templateUrl: './app-header.html',
  styleUrls: ['./app-header.css']
})
export class HeaderComponent implements OnInit {

   @Input()  msg:string;  /*通过 Input  接收父组件传过来的msg*/

   @Input()  name:string; 


   //EventEmitter实现子组件给父组件传值

   @Output() toparent=new EventEmitter();

  constructor() { }
  ngOnInit() {}

  //requestData

  requestData(){

    //调用父组件的方法请求数据

    this.toparent.emit('这是子组件的值');
  }

}

2. 父组件获取子组件值

// app-home.html

<app-header #header></app-header>

// app-home.ts

//ViewChild
import { Component, OnInit, ViewChild } from "@angular/core";

@Component({
  selector: "app-home",
  templateUrl: "./app-home.html",
  styleUrls: ["./app-home.css"]
})
export class ProductComponent implements OnInit {
  @ViewChild("header") header; /*定义子组件 注意括号里面的东西和 #header 对应起来  */

  constructor() {}

  ngOnInit() {}

  getChildData() {
    // this.header.run();   /*执行子组件的方法*/

    alert(this.header.msg); /*获取子组件的数据*/

    //  alert(this.header.name);

    this.header.name = "我是父组件改变后的cart name";
  }
}

// app-header.ts

import { Component, OnInit } from "@angular/core";

@Component({
  selector: "app-header",
  templateUrl: "./app-header.html",
  styleUrls: ["./app-header.css"]
})
export class CartComponent implements OnInit {
  constructor() {}

  public msg = "我是子组件的数据";

  public name = "子组件";

  ngOnInit() {}

  run() {
    alert("这是子组件的方法11");
  }
}

路由之间传参

在网上找到一个不错的写的,分享给大家:

一般我们页面跳转传递参数都是这样的格式:

http://angular.io/api?uid=1&username=moon

但是在SPA单页应用中却是下面的结果居多【初级视频都是这样敷衍的】

http://angular.io/api/1/moon

一、 地址栏传参

实现从product页面跳转到product-detail页面

1:在app-routing.module.ts中配置路由。

const routes: Routes = [
{
    path: 'product',
    component: ProductComponent,
 },
 {
    path: 'product-detail',
    component: ProductDetailComponent,
  }
];

2:在app-routing.module.ts中配置路由。

constructor(
    private router: Router,   //这里需要注入Router模块
){}
 
jumpHandle(){
    //这是在html中绑定的click跳转事件
    this.router.navigate(['product-detail'], {
        queryParams: {
            productId: '1',
            title: 'moon'
        }
    });
}


3:在product-detail.ts中获取传递过来的参数productId、title

constructor(
    private activatedRoute: ActivatedRoute,   //这里需要注入ActivatedRoute模块
) {
    activatedRoute.queryParams.subscribe(queryParams => {
        let productId = queryParams.productId;
        let title = queryParams.title;
    });
}


二、 在查询中传递参数

 



//传递数据
<a [routerLink]="['/stock']" [queryParams]="{id: 1}">股票详情</a>
// http://localhost:4200/stock?id=1



// 接受参数
import { ActivatedRoute } from '@amgular/router';

export class StockComponent implements OnInit {

    private stockId: number;    
    
    constructor(private routeInfo: ActivatedRoute)
    
    ngOnInit() {
        this.stockId = this.routeInfo.snapshot.queryParams['id'];
    }
    
}

三、 在路径中传递参数

//修改配置
const routes: Routes = [
  {path: '', redirectTo: '/index', pathMatch: 'full'},
  {path: 'index', component: IndexComponent},
  {path: 'stock/:id', component: StocksComponent },
  {path: '**', component: ErrorPageComponent }
];


//传递数据
...
<a [routerLink]="['/stock', 1]">股票详情</a>
// http://localhost:4200/stock/1



//接受参数
...
import { ActivatedRoute } from '@amgular/router';

export class StockComponent implements OnInit {

    private stockId: number;    
    
    constructor(private activedRoute: ActivatedRoute)
    
    ngOnInit() {
        this.stockId = this.activedRoute.snapshot.params['id'];
    }
    
}

使用snapshot快照的方式传递数据,因为初始化一次,路由到自身不能传递参数,需要使用订阅模式。

this.activedRoute.params.subscribe((params: Params) => this.stockId = params['id']);

 

四、在路由中配置

//路由配置配置
const routes: Routes = [
  {path: '', redirectTo: '/index', pathMatch: 'full'},
  {path: 'index', component: IndexComponent, data: {title: 'Index Page'}},
  {path: 'stock/:id', component: StocksComponent, data: {title: 'Stock Page'}},
  {path: '**', component: ErrorPageComponent, data: {title: 'Stock Page'}}
];

//接受参数
this.title = this.routeInfo.snapshot.date[0]['title'];

最近学习了 angular 表单。

1. 表单绑定数据,目前有两种方法:

    1. [(ngModel)]=”name”;  // 双向绑定属性值

    2. formControlNam=”name”;  // 按名称FormControl将现有FormGroup的a 同步到表单控件元素。

例如:

import {Component} from '@angular/core';
import {FormControl, FormGroup, Validators} from '@angular/forms';
 
@Component({
  selector: 'example-app',
  template: `
    <form [formGroup]="form" (ngSubmit)="onSubmit()"> <!-- 这里必须写 [formGroup]="form" 绑定下方 new FormGroup()  -->
      <div *ngIf="first.invalid"> Name is too short. </div>
 
      <input formControlName="first" placeholder="First name"> <!-- formControlName 绑定的是下方 new FormGroup() 中 first -->
      <input formControlName="last" placeholder="Last name">
 
      <button type="submit">Submit</button>
   </form>
   <button (click)="setValue()">Set preset value</button>
  `,
})
export class SimpleFormGroup {
  form = new FormGroup({
    first: new FormControl('Nancy', Validators.minLength(2)),  last: new FormControl('Drew'), // 'Nancy' 是 值, validators.minLength(2) 是angular默认表单验证,也可以自定义验证
  });
 
  get first(): any { return this.form.get('first'); } // 必须写 这个方法,之后才可以获取到表单的值
 
  onSubmit(): void {
    console.log(this.form.value);  // {first: 'Nancy', last: 'Drew'} // 获取 值
  }
 
  setValue() { this.form.setValue({first: 'Carson', last: 'Drew'}); } // 改变 first 与 last 表单的值
}

app.module.ts 引入必须 

import { FormsModule, ReactiveFormsModule } from “@angular/forms”;

否者 不可以用 formControlName

注:formControlName 与 ngModel 使用

在Angular v6中不支持将ngModel输入属性和ngModelChange事件与反应式表单指令一起使用,并且将在Angular v7中删除。

现已弃用:

content_copy<form [formGroup]="form"> <input formControlName="first" [(ngModel)]="value"> </form> <!-- 不要一起用了 -->
content_copythis.value = 'some value';

由于一些原因,这已被弃用。首先,开发人员发现这种模式令人困惑。看起来似乎ngModel正在使用实际的指令,但实际上它是ngModel在反应式表单指令上命名的输入/输出属性,它简单地近似(某些)其行为。具体来说,它允许获取/设置值和拦截值事件。然而,ngModel其他一些功能 – 比如延迟更新ngModelOptions或导出指令 – 根本不起作用,这可以理解地引起一些混乱。

此外,这种模式混合了模板驱动和反应形式策略,我们通常不建议这样做,因为它没有利用这两种策略的全部好处。在模板中设置值违反了反应形式背后的模板无关原则,而在类中添加FormControlFormGroup层则消除了在模板中定义表单的便利性。

类型

var a = 1; 隐式; var a:number = 1; 显示声明

var arr = []

类型:

基本类型:

联合类型:—– 选择

函数签名:

success:(a, b)=>void;

复合类型(Object Type):

var a:{x:xx, y:xx};

number, string, boolean, null, undefined, enum, any;

函数类型:

  1. 参数

    • 函数签名
  2. 返回值

function show(a:number, b:number)


外部变量

declare var $;


接口-interface

  • 和 java 不同
  • 接口:约定、限制;

真面向对象

class 写法, extend, 多继承

访问修饰符:

  1. public 共有 任何人可以访问
  2. private 私有 只有类内部可以访问
  3. protected 受保护-友元 只有子类可以访问

泛型– 别跟 “any” 搞混了 泛– 宽泛

class Calc<T> {
    a:T;
    b:T;
    show (c  :T) {
        alert(a + b + c)
    }
} var obj = new Calc<number>();

obj.a = 12;
obj.b = 'abc'; // 报错 }

Array — 泛型

interface Array<T> { 

    reverse() : T[]; 

    sort(compare?: a(a:T, b:T) =>number):T[];

    …

}

// 泛型:类型当参数传入

数组完成写法:var arr:Array<number> = new Array<number>();