使用 max-height 实现动画效果的“展开收起”

当页面中有大量非重要信息时通常使用“展开收起”来处理,常见的作法是在 display: none; 和其它属性之间切换。如果想加上些动画效果通常使用 JavaScript 来实现,若想用 CSS 来实现只需设置 max-height 的值在 0 和一个比目标元素高度大的值之间切换即可。

.box{
    max-height: 0;
    overflow: hidden;
    transition: max-height 1s ease-in;
}
.box.show{
    max-height: 413px;
}

注意

max-height 的值不宜过大,因为动画效果是线性的,所以超过元素本身高度的多余值越多,“收起”时造成视觉上的延迟越大。

Vue 组件通信

一、父组件向子组件通信(Prop)

在子组件里面声明 props 可以接收父组件传过来的值实现父组件向子组件的单向通信。

// 父组件
<son title="后除"></title>

// 子组件
props: {
    titile: {
        type: String,
        required: true
    }
}

二、子组件向父组件通信

1.子组件使用 $emit 触发事件,父组件用 v-on 监听并接收值。

// 父组件
<son @sonEvent="parentListen"></son>

methods: {
    parentListen (msg) {
        console.log(msg)
    }
}

// 子组件
this.$emit('sonEvent', 'I am Mazey!')

2.自定义组件的 v-model

// 父组件
<m-test
    v-model="parentMsg"></m-test>
    {{ parentMsg }}

data: {
    parentMsg: 0
}

// 子组件
<button @click="addsonMsg">addsonMsg</button>
{{ sonMsg }}

data () {
    return {
        sonMsg: 0
    }
},
model: {
    prop: 'sonMsg',
    event: 'sonEvent'
},
methods: {
    addsonMsg () {
        this.$emit('sonEvent', ++this.sonMsg)
    }
}

三、任意组件间通信

注册一个空的 Vue 实例作为中转站,做事件的监听。

// main.js
data: {
    eventHub: new Vue()
}

// 组件A
this.$root.eventHub.$emit('eventName', 'I am Mazey!')

// 组件B
this.$root.eventHub.$on('eventName', msg => {
    console.log(msg)
})

设置 box-sizing 为 border-box 后外边距(margin)会合并吗

border-box 告诉浏览器去理解你设置的边框内边距的值是包含在width内的。也就是说,如果你将一个元素的width设为100px,那么这100px会包含其它的border和padding,内容区的实际宽度会是width减去border + padding的计算值。大多数情况下这使得我们更容易的去设定一个元素的宽高。

设置 box-sizing: border-box; 后,简言之:

  • 盒子的宽度 = border + padding + width
  • 盒子的高度 = border + padding + height

所以 box-sizing: border-box; 对外边距并没有影响,原来会合并的外边距设置完依旧会合并。

<div style="box-sizing: border-box;width: 200px; height: 200px; padding: 20px; margin: 10px; background: #ccc;"></div>
<div style="box-sizing: border-box;width: 200px; height: 200px; padding: 20px; margin: 10px; background: #ccc;"></div>

JavaScript Patterns – Stoyan Stefanov / 拔赤、goddyzhao、TooBug 译

第七章讲了很多设计模式值得一看。

  • JavaScript 中有五种原始类型:数字、字符串、布尔值、null 和 undefined。除了 null 和 undefined 之外,其他三种都有对应的“包装对象”(primitive wrapper object)。可以通过内置构造函数 Number()、String() 和 Boolean() 来生成包装对象。
  • 原始值毕竟不是对象,不能直接对其进行扩充。
  • 那么,到底什么是对象?对象能做这么多事情,那它们一定非常特别。实际上,对象是极其简单的。对象只是很多属性的集合,一个名值对的列表(在其他语言中可能被称作关联数组),这些属性也可以是函数(函数对象),这种函数我们称为“方法”。
  • JavaScirpt 中具有“隐式全局对象”的概念,也就是说任何不通过 var 声明的变量都会成为全局对象的一个属性(可以把它们当作全局变量)。隐式全局变量并不算是真正的变量,但它们却是全局对象的属性。属性是可以通过 delete 运算符删除的,而变量不可以被删除。
  • 代码处理经过了两个阶段:第一阶段是创建变量、函数和形参,也就是预编译的过程,它会扫描整段代码的上下文;第二阶段是在代码的运行时(runtime),这一阶段将创建函数表达式和一些非法的标识符(未声明的变量)。(译注:这两个阶段并没有包含代码的执行,是在执行前的处理过程。)从实用性角度来讲,我们更愿意将这两个阶段归成一个概念“变量提前”,尽管这个概念并没有在 ECMAScript 标准中定义,但我们常常用它来解释预编译的行为过程。
  • 每次遍历都会访问数组的 length 属性,这会降低代码运行效率,特别是当 myarray 不是一个数组而是一个 HTMLCollection 对象的时候。这些对象的问题在于,它们都会实时查询文档(HTML 页面)中的对象。也就是说每次通过它们访问集合的 length 属性时,总是都会去查询 DOM,而 DOM 操则是很耗资源的。
  • 这里介绍一种格式上的变种,这种写法在 for 循环所在的行加入了 if 判断条件,他的好处是能让循环语句读起来更完整和通顺(“如果元素包含属性 X,则对 X 做点什么”):
var i,
    hasOwn = Object.prototype.hasOwnProperty;
for (i in man) if (hasOwn.call(man, i)) { // 过滤
    console.log(i, ":", man[i]);
}
  • new Function() 的用法和 eval() 非常类似,应当特别注意。这种构造函数的方式很强大,但经常会被误用。如果你不得不使用 eval(),你可以尝试用 new Function() 来代替。这有一个潜在的好处,在 new Function() 中运行的代码会在一个局部函数作用域内执行,因此源码中所有用 var 定义的变量不会自动变成全局变量。还有一种方法可以避免 eval() 中定义的变量被转换为全局变量,即是将 eval() 包装在一个即时函数内。
  • 首字母大写可以提示你这是一个构造函数,而首字母小写的函数一般只认为它是普通的函数,不应该通过 new 来调用它。
  • 最早利用注释生成 API 文档的工具诞生自Java业界,这个工具名叫“javadoc”,和 Java SDK(软件开发工具包)一起提供,但这个创意迅速被其他语言借鉴。JavaScript 领域有两个非常优秀的开源工具,它们是 JSDoc Toolkit(http://code.google.com/p/jsdoc-toolkit/)和 YUIDoc(http://yuilibrary.com/projects/yuidoc)。
  • 避免“声明提前”给程序逻辑带来的影响 for 循环、for-in 循环、switch 语句、“避免使用 eval()”、不要扩充内置原型。
  • 在 JavaScript 中根本不存在真正的空对象,理解这一点至关重要。即使最简单的 {} 对象也会包含从 Object.prototype 继承来的属性和方法。我们提到的“空(empty)对象”只是说这个对象没有自有属性(own properties),不考虑它是否有继承来的属性。
  • 我们知道,构造函数和普通的函数本质一样,只是通过 new 调用而已。那么如果调用构造函数时忘记 new 会发生什么呢?漏掉 new 不会产生语法错误也不会有运行时错误,但可能会造成逻辑错误,导致执行结果不符合预期。这是因为如果不写 new 的话,函数内的 this 会指向全局对象(在浏览器端this指向window)。
  • 有些人用 Array() 构造器来做一些有意思的事情,比如用来生成重复字符串。下面这行代码返回的字符串包含255个空格。
var white = new Array(256).join(' ');
  • ECMAScript5 定义了一个新的方法 Array.isArray(),如果参数是数组的话就返回 true。如果你的开发环境不支持 ECMAScript5,可以通过 Object.prototype.toString() 方法来代替。如调用 toString 的 call() 方法并传入数组上下文,将返回字符串“[object Array]”。如果传入对象上下文,则返回字符串“[object Object]”。因此可以这样做:
if (typeof Array.isArray === "undefined") {
    Array.isArray = function (arg) {
        return Object.prototype.toString.call(arg) === "[object Array]";
    };
}
  • throw 可以抛出任何对象,并不限于“错误对象”,因此你可以根据需要抛出自定义的对象。这些对象包含属性“name”和“message”或其他你希望传递给异常处理逻辑的信息,异常处理逻辑由 catch 语句指定。你可以灵活运用抛出的错误对象,将程序从错误状态恢复至正常状态。
  • JavaScript 的函数具有两个主要特性,正是这两个特性让它们与众不同。第一个特性是,函数是一等对象(first-class object),第二个是函数提供作用域支持。
  • 在 JavaScript 中没有块级作用域(译注:在JavaScript1.7中提供了块级作用域部分特性的支持,可以通过let来声明块级作用域内的“局部变量”),也就是说不能通过花括号来创建作用域,JavaScript 中只有函数作用域(译注:这里只针对函数而言,此外 JavaScript 还有全局作用域)。在函数内所有通过 var 声明的变量都是局部变量,在函数外部是不可见的。刚才所说的花括号无法提供作用域支持的意思是说,如果在 if 条件句、for 或 while 循环体内用 var 定义了变量,这个变量并不是属于 if 语句或 for(while)循环的局部变量,而是属于它所在的函数。如果不在任何函数内部,它会成为全局变量。
  • apply() 接受两个参数:第一个是在函数内部绑定到 this 上的对象,第二个是一个参数数组,参数数组会在函数内部变成一个类似数组的 arguments 对象。如果第一个参数为 null,那么 this 将指向全局对象,这正是当你调用一个函数(且这个函数不是某个对象的方法)时发生的事情。
  • 使用链式调用模式可以让你在一对个象上连续调用多个方法,不需要将前一个方法的返回值赋给变量,也不需要将多个方法调用分散在多行。当你创建了一个没有有意义的返回值的方法时,你可以让它返回 this,也就是这些方法所属的对象。这使得对象的使用者可以将下一个方法的调用和前一次调用链起来。
  • 使用构造函数就像 Java 中使用类一样。它也允许你在构造函数体的 this 中添加实例属性。但是在 this 中添加方法却是不高效的,因为最终这些方法会在每个实例中被重新创建一次,这样会花费更多的内存。这也是为什么可重用的方法应该被放到构造函数的 prototype 属性(原型)中的原因。但对很多开发者来说,prototype 可能跟个外星人一样陌生,所以你可以通过一个方法将它隐藏起来。
  • constructor 属性很少被用到,但是在运行时检查对象很方便。你可以重新将它指向期望的构造函数而不影响功能,因为这个属性更多是“信息性”的。(译注:即它更多的时候是在提供信息而不是参与到函数功能中。)
function inherit(C, P) {
    var F = function () {};
    F.prototype = P.prototype;
    C.prototype = new F();
    C.uber = P.prototype;
    C.prototype.constructor = C;
}
  • 有时候会有这样的情况:你希望使用某个已存在的对象的一两个方法,你希望能复用它们,但是又真的不希望和那个对象产生继承关系,因为你只希望使用你需要的那一两个方法,而不继承那些你永远用不到的方法。得益于函数的 call() 和 apply() 方法,可以通过借用方法模式实现它。你传一个对象和任意的参数,这个被借用的方法会将 this 绑定到你传递的对象上。简单地说,你的对象会临时假装成另一个对象以使用它的方法。这就像实际上获得了继承但又免除了“继承税”(译注:指不需要的属性和方法)。
  • ECMAScript5 在 Function.prototype 中添加了一个方法叫 bind(),使用时和 apply()/call() 一样简单。
  • 外观模式也适用于一些浏览器脚本的场景,即将浏览器的差异隐藏在一个外观方法下面。添加一些处理IE中事件API的代码:
var myevent =  {
    // ……
    stop: function (e) {
        // 其它浏览器
        if (typeof e.preventDefault === "function") {
            e.preventDefault();
        }
        if (typeof e.stopPropagation === "function") {
            e.stopPropagation();
        }
        // IE
        if (typeof e.returnValue === "boolean") {
            e.returnValue = false;
        }
        if (typeof e.cancelBubble === "boolean") {
            e.cancelBubble = true;
        }
    }
    // ……
};
  • 观察者模式被广泛地应用于 JavaScript 客户端编程中。所有的浏览器事件(mouseover,keypress 等)都是使用观察者模式的例子。这种模式的另一个名字叫“自定义事件”,意思是这些事件是被编写出来的,和浏览器触发的事件相对。它还有另外一个名字叫“订阅者/发布者”模式(Pub/Sub)。使用这个模式的最主要目的就是促进代码解耦。在观察者模式中,一个对象订阅另一个对象的指定活动并得到通知,而不是调用另一个对象的方法。订阅者也被叫作观察者,被观察的对象叫作发布者或者被观察者。当一个特定的事件发生的时候,发布者会通知(调用)所有的订阅者,同时还可能以事件对象的形式传递一些消息。
  • 浏览器中有非常多不一致的宿主对象和 DOM 实现。很明显,任何能够减轻客户端脚本编程的痛楚的最佳实践都是大有益处的。
  • DOM 访问的次数应该被减少到最低,这意味者:1.避免在循环中访问 DOM。2.将 DOM 引用赋给本地变量,然后操作本地变量。3.当可能的时候使用 selectors API。4.遍历 HTML collections 时缓存 length。
  • 除了访问 DOM 元素之外,你可能经常需要改变它们、删除其中的一些或者是添加新的元素。更新 DOM 会导致浏览器重绘(repaint)屏幕,也经常导致重排(reflow,重新计算元素的位置),这些操作代价是很高的。还是那句话,原则是尽量少地更新 DOM,这意味着我们可以将变化集中到一起,然后在“活动的”(live)文档树之外去执行这些变化。当你需要添加一棵相对较大的子树的时候,你应该在完成这棵树的构建之后再放到文档树中。为了达到这个目的,你可以使用文档碎片(document fragment)来包含你的节点。
  • 创建一个文档碎片,然后“离线地”(译注:即不在文档树中)更新它,当它准备好之后再将它加入文档树中。当你将文档碎片添加到 DOM 树中时,碎片的内容将会被添加进去,而不是碎片本身。这个特性非常好用。所以当有好几个没有被包裹在同一个父元素的节点时,文档碎片是一个很好的包裹方式。
var p, t, frag;
frag = document.createDocumentFragment();
p = document.createElement('p');
t = document.createTextNode('first paragraph');
p.appendChild(t);
frag.appendChild(p);
p = document.createElement('p');
t = document.createTextNode('second paragraph');
p.appendChild(t);
frag.appendChild(p);
document.body.appendChild(frag);
  • setTimeout() 1毫秒(甚至0毫秒)的延迟执行命令在实际运行的时候会延迟更多,这取决于浏览器和操作系统。设定0毫秒的延迟并不意味着马上执行,而是指“尽快执行”。比如,在IE中,最短的延迟是15毫秒。
  • script 元素会阻塞页面的下载。浏览器会同时下载好几个组件(文件),但遇到一个外部脚本的时候,会停止其它的下载,直到脚本文件被下载、解析、执行完毕。这会严重影响页面的加载时间,尤其是当页面加载时发生多次阻塞的时候。为了尽量减小阻塞带来的影响,你可以将 script 元素放到页面的尾部,在 之前,这样就没有可以被脚本阻塞的元素了。此时,页面中的其它组件(文件)已经被下载完毕并呈现给用户了。
  • 通常脚本是插入到文档的 中的,但其实你可以插入任何元素中,包括 (像 JSONP 那样)。在前面的例子中,我们使用 documentElement 来插到 中,因为 documentElement 就是 ,它的第一个子元素是 。当你能控制结构的时候,这样做没有问题,但是如果你在写挂件(widget)或者是广告时,你并不知道使用它的是一个什么样的页面。甚至可能页面上连 和 都没有,尽管 document.body 在绝大多数没有 标签的时候也可以工作。可以肯定页面上一定存在的一个标签是你正在运行的脚本所处的位置 —— script 标签。(对内联或者外部文件来说)如果没有 script 标签,那么代码就不会运行。可以利用这一事实,在页面的第一个 script 标签上使用 insertBefore():
var first_script = document.getElementsByTagName('script')[0];
first_script.parentNode.insertBefore(script, first_script);

使用 postMessage + iframe 实现跨域通信

一、postMessage

window.postMessage() 方法可以安全地实现跨源通信。通常,对于两个不同页面的脚本,只有当执行它们的页面位于具有相同的协议(通常为https),端口号(443为https的默认值),以及主机(两个页面的模数 Document.domain设置为相同的值)时,这两个脚本才能相互通信。window.postMessage() 方法提供了一种受控机制来规避此限制,只要正确的使用,这种方法就很安全。

调用 postMessage() 方法时,向目标窗口派发一个 Event 消息。 该 Event 消息的 data 属性为 postMessage() 的数据;origin 属性表示调用 postMessage() 方法的页面的地址。

二、语法

otherWindow.postMessage(message, targetOrigin)

说明

  • otherWindow:其他窗口的一个引用,比如 iframe 的 contentWindow 属性、执行 window.open 返回的窗口对象、或者是命名过或数值索引的 window.frames。
  • message:将要发送到其他 window 的数据。它将会被结构化克隆算法序列化。这意味着你可以不受什么限制的将数据对象安全的传送给目标窗口而无需自己序列化。
  • targetOrigin:通过窗口的 origin 属性来指定哪些窗口能接收到消息事件,其值可以是字符串 * (表示无限制)或者一个 URI。在发送消息的时候,如果目标窗口的协议、主机地址或端口这三者的任意一项不匹配 targetOrigin 提供的值,那么消息就不会被发送;只有三者完全匹配,消息才会被发送。

三、示例

我配了两个域名 example.mazey.cnexample0.mazey.cn,其中 http://example0.mazey.cn/post-message/send.html 会给 http://example.mazey.cn/post-message/receive.html 发送一条消息 Message From Mazey.

send.html

<script>
  window.onload = function () {
    window.parent.postMessage(
      'Message From Mazey.',
      '*'
    )
  }
</script>

receive.html

<script>
  // 监听
  window.addEventListener(
    'message',
    event => {
      let origin = event.origin || event.originalEvent.origin
      console.log(event, origin, event.data) // MessageEvent{...} "http://example0.mazey.cn" "Message From Mazey."
    }
  )
  // 使用 JS 加载 iframe,保证监听创建后再加载 iframe。
  let iframeEle = document.createElement('iframe')
  iframeEle.src = 'http://example0.mazey.cn/post-message/send.html'
  iframeEle.style = 'border: none;width: 0;height: 0;'
  document.body.insertBefore(iframeEle, document.body.firstChild)
</script>

注意

如果发送的消息有敏感信息(例如:密码),始终验证接受时的 event.origin 和指定发送时的 targetOrigin。

基于 Vue 的前端开发规范

一、框架 / Vue

1.组件名

组件名为多个单词,并且用连接线(-)连接,避免与 HTML 标签冲突,并且结构更加清晰。

示例:

// 反例
export default {
    name: 'item'
}

// 正例
export default {
    name: 'page-article-item'
}

2.组件文件

组件的名字应该始终是以连接线(-)连接的单词,一方面可与组件名一致,使项目更加清晰,另一方面这样的写法对编辑器引入也很友好。

示例:

// 反例
├── index.html
├── main.js
└── components
    ├── pageheader
    ├── pagearticle
    └── pageheader

// 正例
├── index.html
├── main.js
└── components
    ├── page-header
    ├── page-article
    └── page-header

对于例如按钮、下拉框或表格这样的基础组件应该始终以一个特定的前缀开头,区别与其他业务组件。

示例:

// 反例
├── index.html
├── main.js
└── components
    ├── page-header
    ├── page-article
    ├── page-header
    ├── mazey-button
    ├── mazey-select
    └── mazey-table

// 正例
├── index.html
├── main.js
└── components
|   ├── page-header
|   ├── page-article
|   └── page-header
└── base
    ├── mazey-button
    ├── mazey-select
    └── mazey-table

3.Prop

定义 Prop 的时候应该始终以驼峰格式(camelCase)命名,在父组件赋值的时候使用连接线(-)。这里遵循每个语言的特性,因为在 HTML 标记中对大小写是不敏感的,使用连接线更加友好;而在 JavaScript 中更自然的是驼峰命名。

示例:

// 反例
// Vue
props: {
    article-status: Boolean
}
// HTML
<article-item :articleStatus="true"></article-item>

// 正例
// Vue
props: {
    articleStatus: Boolean
}
// HTML
<article-item :article-status="true"></article-item>

Prop 的定义应该尽量详细的指定其类型、默认值和验证。

示例:

// 反例
props: ['attrM', 'attrA', 'attrZ']

// 正例
props: {
    attrM: Number,
    attrA: {
        type: String,
        required: true
    },
    attrZ: {
        type: Object,
        // 数组/对象的默认值应该由一个工厂函数返回
        default: function () {
            return {
                msg: '成就你我'
            }
        }
    },
    attrE: {
        type: String,
        validator: function (v) {
            return !(['success', 'fail'].indexOf(v) === -1) 
        }
    }
}

4.v-for

在执行 v-for 遍历的时候,总是应该带上 key 值使更新 DOM 时渲染效率更高。

示例:

// 反例
<ul>
    <li v-for="item in list">
        {{ item.title }}
    </li>
</ul>

// 正例
<ul>
    <li v-for="item in list" :key="item.id">
        {{ item.title }}
    </li>
</ul>

v-for 应该避免与 v-if 在同一个元素(例如:<li>)上使用,因为 v-for 的优先级比 v-if 更高,为了避免无效计算和渲染,应该尽量将 v-if 放到容器的父元素之上。

示例:

// 反例
<ul>
    <li v-for="item in list" :key="item.id" v-if="showList">
        {{ item.title }}
    </li>
</ul>

// 正例
<ul v-if="showList">
    <li v-for="item in list" :key="item.id">
        {{ item.title }}
    </li>
</ul>

5.v-if / v-else-if / v-else

若同一组 v-if 逻辑控制中的元素逻辑相同,Vue 为了更高效的元素切换,会复用相同的部分,例如:value。为了避免复用带来的不合理效果,应该在同种元素上加上 key 做标识。

示例:

// 反例
<div v-if="hasData">
    <span>{{ mazeyData }}</span>
</div>
<div v-else>
    <span>无数据</span>
</div>

// 正例
<div v-if="hasData" key="mazey-data">
    <span>{{ mazeyData }}</span>
</div>
<div v-else key="mazey-none">
    <span>无数据</span>
</div>

6.指令缩写

为了统一规范始终使用指令缩写,使用v-bindv-on并没有什么不好,这里仅为了统一规范。

示例:

<input :value="mazeyUser" @click="verifyUser">

7.样式

为了避免样式冲突,整个项目要么全都使用 scoped 特性,要么使用 BEM 约定。

示例:

// 反例
<template>
    <button class="btn btn-sure">确认</button>
</template>
<style>
    .btn{
        border: 1px solid #F1F1F1;
    }
    .btn-sure{
        background-color: blue;
    }
</style>

// 正例
<!-- 使用 scoped 特性 -->
<template>
    <button class="btn btn-sure">确认</button>
</template>
<style scoped>
    .btn{
        border: 1px solid #F1F1F1;
    }
    .btn-sure{
        background-color: blue;
    }
</style>

<!-- 使用 BEM(Block Element Modifier) 特性 -->
<template>
    <button class="menu-btn menu-btn-sure">确认</button>
</template>
<style>
    .menu-btn{
        border: 1px solid #F1F1F1;
    }
    .menu-btn-sure{
        background-color: blue;
    }
</style>

注意

不建议使用类似于 font-size-20color-666margin-top-20这样的类,此时如果想把全局的color-666颜色改成 #ccc,不管是直接把 .color-666{color: #666;} 改成 .color-666{color: #ccc;},还是 全局搜索修改 class 名都很麻烦。本质上我认为 color-666 和直接写 style="color: #666;" 并没有区别。

8.单文件组件的顶级元素顺序

为了统一和便于阅读,应该按 <template><script><style>的顺序放置。

示例:

// 反例
<style>
/* CSS */
</style>
<script>
/* JavaScript */
</script>
<template>
<!-- HTML -->
</template>

// 正例
<template>
<!-- HTML -->
</template>
<script>
/* JavaScript */
</script>
<style>
/* CSS */
</style>

二、JavaScript

1.var / let / const

建议不再使用 var,而使用 let / const,优先使用 const。任何一个变量的使用都要提前申明,除了 function 定义的函数可以随便放在任何位置。

2.引号

建议不再使用双引号,静态字符串使用单引号,动态字符串使用反引号衔接。

示例:

// 反例
const foo = "后除"
const bar = foo + ",前端工程师"

// 正例
const foo = '后除'
const bar = `${foo},前端工程师`

3.函数

匿名函数统一使用箭头函数,多个参数/返回值时优先使用对象的结构赋值。

示例:

// 反例
function getPersonInfo (name, sex) {
    // ...
    return [name, gender]
}

// 正例
function getPersonInfo ({name, sex}) {
    // ...
    return {name, gender}
}

函数名统一使用驼峰命名,以大写字母开头申明的都是构造函数,使用小写字母开头的都是普通函数,也不该使用 new 操作符去操作普通函数。

4.对象

建议使用扩展运算符拷贝对象而不是 Object.assign(target, ...sources)

示例:

// 错误
const foo = {a: 0, b: 1}
const bar = Object.assign(foo, {c: 2})

// 反例
const foo = {a: 0, b: 1}
const bar = Object.assign({}, foo)

// 正例
const foo = {a: 0, b: 1}
const bar = JSON.parse(JSON.stringify(foo))

// 极好
const foo = {a: 0, b: 1}
const bar = {...foo, c: 2}

对象尽量静态化,添加新属性使用 Object.assign(target, ...sources)

示例:

// 反例
const foo = {a: 3}
foo.b = 4

// 正例
const foo = {a: 3}
Object.assign(foo, {b: 4})

若有遍历对象的需求,优先使用 Map 结构。

示例:

// 反例
const myMap = {
    foo: 0,
    bar: 1
}
for (let key in myMap) {
    // ...
}

// 正例
const myMap = new Map([])
for (let [key, value] of myMap.entries()) {
    // ...
}

5.模块

统一使用 import / export 的方式管理项目的模块。

示例:

// lib.js
export default {}

// app.js
import app from './lib'

import 统一放在文件顶部。

如果模块只有一个输出值,使用 export default,否则不用。

6.循环

for (var i = 0; i < arr.length; i++) {} 这样的方式遍历不是很好,尤其当 arr 是 Dom 对象的时候,这样就会一直在访问 Dom 层,访问 Dom 层的代价是很大的。for (var i = 0, j=arr.length; i < j; i++) {} 这样的方式去用循环是比较好的,只会访问一次 Dom 层(不适用于 Dom 节点会动态更新的场景)。

7.eval

避免使用 eval,如要进行字符串转化为对象,最好使用浏览器的内置方法来解析 JSON 数据,以确保代码的安全性和数据的合法性。如果浏览器不支持 JSON.parse(),你可以使用 JSON.org 所提供的库。如果不得不使用 eval(),可以尝试用 new Function() 来代替,在 new Function() 中运行的代码会在一个局部函数作用域内执行,因此源码中定义的变量不会自动变成全局变量。

三、HTML / CSS

1.标签

在引用外部 CSS 或 JavaScript 时不写 type 属性。HTML5 默认 type 为 text/csstext/javascript 属性,所以没必要指定。

示例:

// 反例
<link rel="stylesheet" href="//www.test.com/css/test.css" type="text/css">
<script src="//www.test.com/js/test.js" type="text/javascript"></script>

// 正例
<link rel="stylesheet" href="//www.test.com/css/test.css">
<script src="//www.test.com/js/test.js"></script>

2.协议

省略协议头部声明,例如:http:https:,且不加引号。

示例:

// 反例
background: url("http://www.test.com/img/test.png");

// 正例
background: url(//www.test.com/img/test.png);

3.选择器

选择器尽可能简单,能使用 .test 别使用 div.test

每个选择器和声明都要独立新行。

示例:

// 反例
p.test{
    color: #000;
}
h1,h2,h3{
    font-size: 12px;
    color: #666;
}

// 正例
.test{
    color: #000;
}
h1,
h2,
h3{
    font-size: 12px;
    color: #666;
}

4.分号

每个声明最后都要加分号 ;,即使是最后一个。

示例:

// 反例
h3{
    font-size: 12px;
    color: #666
}

// 正例
h3{
    font-size: 12px;
    color: #666;
}

5.命名

Class 和 ID 的命名应该语义化,通过看名字就知道是干嘛的;多个单词用连接线 - 连接。

示例:

// 反例
.testheader{
    font-size: 20px;
}

// 正例
.test-header{
    font-size: 20px;
}

6.属性缩写

CSS 属性尽量使用缩写,提高代码的效率和方便理解。

示例:

// 反例
border-width: 1px;
border-style: solid;
border-color: #ccc;

// 正例
border: 1px solid #ccc;

7.文档类型

应该总是使用 HTML5 标准。

示例:

<!DOCTYPE html>

8.table

应该避免使用 table 做页面布局,因为同样的布局使用 table 要多几倍的渲染时间;当然使用 table 做表格是完全正确的。

9.注释

应该给一个模块文件写一个区块注释。

示例:

/**
* @module mazey/api
* @author Mazey <[email protected]>
* @description test.
* */

浅谈前后端分离

一、介绍

前和后自古以来就是一对非常有意思的组合,比如空前绝后,前无古人后无来者,就像道家的阴阳一样,既是相铺相成又是剪不断理还乱。

前后端分离最近两年非常流行,但在我刚开始工作的时候只知道网站分为静态网站和动态网站。当时需求提出来之后,先是由设计师把视觉稿做出来,然后由前端把整个网站的静态页面写出来,后端再把静态页面转成动态渲染的页面。

前端在写页面的时候会用假数据填充页面,然后后端等前端把静态页面写完以后拿过来套真实的数据,然后就产生了这样前后端高耦合的页面。如果配合好的话还好,不过一般来说都不会太顺利,特别是曾经的我是一个不懂前端的后端,每次转完动态后都出现了一系列不可预知的问题,这时候我又得找前端去解决,然后前端拿到我的动态页面代码也是一脸懵逼。在之后如果要改需求改样式的话也很难改,基本需要两个人配合着改,开发周期又长又痛苦。

静态页面

<article>
    <header>注意!本周亚马逊下架了这类产品,卖家小心踩雷</header>
    <div>1、注意!本周亚马逊下架了这类产品,卖家小心踩雷 亚马逊上有这么一款产品,外包装上刻画了一个患有强迫症的男子,…</div>
    <footer>小易快讯</footer>
</article>
<article>
    <header>注意!本周亚马逊下架了这类产品,卖家小心踩雷</header>
    <div>1、注意!本周亚马逊下架了这类产品,卖家小心踩雷 亚马逊上有这么一款产品,外包装上刻画了一个患有强迫症的男子,…</div>
    <footer>小易快讯</footer>
</article>
<article>
    <header>注意!本周亚马逊下架了这类产品,卖家小心踩雷</header>
    <div>1、注意!本周亚马逊下架了这类产品,卖家小心踩雷 亚马逊上有这么一款产品,外包装上刻画了一个患有强迫症的男子,…</div>
    <footer>小易快讯</footer>
</article>

动态页面

<?php
// ...
$name = $_GET['act'];
$page = $_GET['page'];
$rows = array();
$sql = "select post_id, post_title, post_content ... limit 10;";
$rs = $mysqli->query($sql);
if($rs && $rs->num_rows > 0):
    while($row = $rs->fetch_assoc()):
        $rows[] = $row;
// ...
/* 循环输出文章 */
foreach($rows as $row):
?>
<article>
    <header><?php echo $row['post_title']; ?><</header>
    <div><?php echo $row['post_content']; ?></div>
    <footer>小易快讯</footer>
</article>
<?php
endforeach;
?>

然后 PC 端和移动端的逻辑又不太一样,就催生出了 Bootstrap 这样的响应式布局,一套代码多个尺寸适用,但是往往达不到想要的效果,或者某个尺寸会不那么好。

二、全端/栈工程师

然后这时候如果一个人又懂前端又懂后端就是很方便,所以常常在面试要求里看到前端岗位都要会用一种后端语言,后端也要对 HTML/CSS/JS 有一定的了解;既然是全端开发,就一定涉及到非常非常多的技术。每一项都掌握得非常精,几乎是不可能的,至少对于我这样的普通人是不可能,人的精力实在是太有限了,常常被人觉得简单的 SQL、CSS、正则表达式稍微深一点都是需要大量的时间去研究,所以我认为首先有一项自己比较精通的技术,然后广度一定要达到。

于个人而言,找到自己喜欢的方向,更容易成为这个领域的专家;于招聘而言招聘成本也会降低,目标更明确;在实际项目中合适的人做合适的事情,这样项目质量也会更高。

三、前后端分离

前后端分离我的理解其实前后端分工,在项目中后端只需要提供 JSON 数据,前端负责接收数据渲染和与后端交互。此时前端除了写页面之外还负责数据处理过滤和业务逻辑方面的工作,后端可以专注于业务数据方面。

此时项目可以分为三个阶段:

  1. 设计阶段:前后端负责人就项目进行讨论,约定接口格式。
  2. 开发阶段:前后端开发人员各自分工,后端提供接口文档,前端根据文档模拟数据渲染页面。
  3. 测试阶段:若后端接口没有完成,前端可以根据模拟数据先进行测试,接口完成后再联调。

H5应用程序缓存 – Cache manifest

一、作用

离线浏览 – 根据文件规则把资源缓存在本地,脱机依然能够访问资源,联网会直接使用缓存在本地的文件。优化加载速度,节约服务器资源。

二、适用场景

正如 manifest 英译的名字:离线应用程序缓存,这项功能是设计给会有离线场景的应用来使用,例如:需要离线使用的手机APP/H5,亦或是PC端的纯静态页面。

三、问题

1.服务器资源更新后显示滞后需要多刷新一次页面

  1. 首次在载入页面时,浏览器会判断是否引入了 manifest,若检测到引入,则下载并按文件规则缓存资源。
  2. 再次载入页面时,便会根据 manifest 中规定的缓存文件使用本地缓存而不是请求服务器资源。
  3. 当 manifest 文件更新时,页面加载已经进行,但是缓存更新尚未完成,便会先使用旧 manifest 所缓存的文件,同时浏览器会将新的 manifest 文件里的缓存文件下载下来,若想看到最新的静态资源需要刷新下页面重新加载一次。

2.全量加载

当 manifest 文件更新时,所有的资源都要全部被下载一次,并且其中一个出现异常会导致整个 manifest 运行异常。

3.W3C标准及浏览器支持

作为 H5 的新属性,初衷是用来构造离线应用的,缓存网站资源的话原本浏览器的缓存机制(304)已经很好了,没必要再使用 manifest 做缓存;实际上因为使用反响并不好,W3C正在废弃 manifest。

四、使用方法

首先在标签 <html> 中指定 manifest 配置文件。

<html manifest="manifest.appcache">

配置文件 manifest.appcache

CACHE MANIFEST
# 井号备注,这边可以放版本更新时间,修改备注算更新 manifest 文件
CACHE:
# 首次下载 CACHE 后列出来的文件后缓存到本地。
/favicon.ico
/main.css

NETWORK:
# NETWORK 下的文件不可被缓存,必须要与服务器连接。
login.php

FALLBACK:
# 一行放两个路径,第一个是访问资源,第二个是替补;当无法访问前一个资源时,用后一个资源代替之。
/cat/ /404.html

注意

manifest 会把当前页面缓存下来,所以若想更新当前页,只能先更新 manifest 文件。

五、总结

manifest 适用于需要离线运行的应用(定时器,计算器,阅读器等工具);而需要经常发布频繁更新的网站,这种离线缓存机制并不适合。

Webpack4.x 入门

概览

新建项目

npm init -y

安装webpack & webpack-cli

(c)npm install -D webpack
(c)npm install -D webpack-cli

# 查看webpack版本
(npx )webpack --version

试打包

src/index.js

document.write('Hello Webpack -Mazey')

dist/index.html

<!doctype html>
<html>
    <head>
        <title>Start Webpack</title>
    </head>
    <body>
        <script src="bundle.js"></script>
    </body>
</html>

webpack.config.js

const path = require('path')

module.exports = {
    entry: './src/index.js',
    output: {
        filename: 'bundle.js',
        path: path.resolve(__dirname, 'dist')
    }
}

项目命令行运行:

webpack --config webpack.config.js --mode development

# 出现
Hash: 1ae48c1f7dc49168e983
Version: webpack 4.6.0
Time: 63ms
Built at: 2018-05-03 14:37:02
    Asset      Size  Chunks             Chunk Names
bundle.js  2.84 KiB    main  [emitted]  main
Entrypoint main = bundle.js
[./src/index.js] 38 bytes {main} [built]

此时 dist/ 下多了一个 bundle.js 文件, 打开 dist/index.html 出现 Hello Webpack -Mazey

在package.json定制脚本

{
  // ...
  "scripts": {
    "dev": "webpack --config webpack.config.js --mode development",
    "build": "webpack --mode production"
  },
  // ...
}


# or

{
  // ...
  "scripts": {
    "dev": "webpack-dev-server --devtool eval --progress --colors",
    "deploy": "NODE_ENV=production webpack -p"
  },
  // ...
}

然后命令行运行 npm run dev 便等于 webpack --config webpack.config.js --mode development

一、入口[entry]

语法

1.单个入口语法

entry: string|Array<string>

示例:

// ...
entry: './src/index.js'
// ...

# 等于

// ...
entry: {
    main: './src/index.js'
}
// ...

2.对象语法

entry: {[entryChunkName: string]: string|Array<string>}

示例:

// ...
entry: {
    app: './src/app.js',
    vendors: './src/vendors.js'
}
// ....

二、输出[output]

语法

// ...
output: {
    filename: <output filename>,
    path: <path>
}
// ...
  • filename: 用于输出文件的文件名。
  • path: 目标输出目录的绝对路径。

示例:

const path = require('path')

const config = {
    entry: './src/index.js',
    output: {
        filename: 'bundle.js',
        path: path.resolve(__dirname, 'dist')
    }
}

module.exports = config

若配置多个入口,为保证每个文件具有唯一名称,需要用到占位符

// ...
filename: '[name].js',
// ...

三、模式[mode]

语法

mode: string
  • development: 会将 process.env.NODE_ENV 的值设为 development。启用 NamedChunksPlugin 和 NamedModulesPlugin。
  • production: 会将 process.env.NODE_ENV 的值设为 production。启用 FlagDependencyUsagePlugin, FlagIncludedChunksPlugin, ModuleConcatenationPlugin, NoEmitOnErrorsPlugin, OccurrenceOrderPlugin, SideEffectsFlagPlugin 和 UglifyJsPlugin。

设置 模式 后则不需要在命令后带上 --mode development

四、载入器?[loader]

语法

module: {
    rules: [
      { test: <.*>, use: <loader> },
      { test: <.*>, use: <loader> }
    ]
}
  • test: 标识出应该被对应的 loader 进行转换的某个或某些文件。
  • use: 表示进行转换时,应该使用哪个 loader。

示例:

const path = require('path')

const config = {
  // ...
  module: {
    rules: [
      { test: /\.css$/, use: 'css-loader' }
    ]
  }
}

module.exports = config

碰到“在 require()/import 语句中被解析为 .css 的路径”时,打包之前,先使用 css-loader 转换一下。

五、插件[plugins]

语法

const <PluginName> = require(<plugin-name>)
// ...
plugins: [
    new <PluginName>({
        <attribute>: <value>
    })
]

想要使用一个插件,你只需要 require() 它,然后把它添加到 plugins 数组中。

用Nginx反向代理Node.js

安装pm2

npm install pm2 -g

ln -s /home/download/node-v8.11.1-linux-x64/lib/node_modules/pm2/bin/pm2 /usr/local/bin/pm2

修改package.json

"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "pm2": "/home/download/node-v8.11.1-linux-x64/lib/node_modules/pm2/bin/pm2 start /web/mazey.cn/server/app.js"
}

or

"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "pm2": "pm2 start app.js"
}

启动pm2

npm run pm2

开机启动pm2

pm2 save

pm2 startup centos

注意

pm2 startup centos失败,可尝试pm2 startup

 PM2 detected systemv but you precised centos
 Please verify that your choice is indeed your init system
 If you arent sure, just run : pm2 startup

修改Nginx配置

vim /etc/nginx/conf.d/*.conf

upstream nodejs {
    server 127.0.0.1:3000;
    keepalive 64;
}
server {
    listen 80;
    server_name domain.cn;
    root /web/mazey.cn;
    index index.html index.htm;
    # 网站切到/server下时走nodejs
    location /server {
        proxy_set_header    X-Real-IP $remote_addr;
        proxy_set_header    X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header    Host  $http_host;
        proxy_set_header    X-Nginx-Proxy true;
        proxy_set_header    Connection "";
        proxy_pass http://nodejs;
    }
    location ~ .*\.(gif|jpg|jpeg|png|bmp|swf)$ {
        expires 30d;
    }
    location ~ .*\.(js|css)?$ {
        expires 1h;
    }
}

相应的 app.js

const express = require('express')
const app = express()
let hi = 'hi'

app.get('/server', (req, res, next) => {
  hi = `Hello Mazey!\n`
  next()
}, (req, res) => {
  res.send(`
  ${hi}
  ${req.method}\n
  ${req.originalUrl}\n
  ${req.query.id}\n
  `)
})

const server = app.listen(3000, function () {
  let host = server.address().address
  let port = server.address().port

  console.log('Example app listening at http://%s:%s', host, port)
})

注意

若报错 Cannot GET /xxx 说明 Express 的路由没配好。