Giter Site home page Giter Site logo

blog's People

Contributors

hugo-seth avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

Forkers

isslayne

blog's Issues

【译】Vue.js 教程:构建并提前渲染一个 SEO 友好的网站

原文地址:Vue.js Tutorial: A Prerendered, SEO-Friendly Example [Live Demo] - Snipcart

原文作者:Maxime Laboissonniere

“受不了了,我们内部的报告面板太难用了”,我们的生产经理生气的说。他尝试下载数据的 app 简直是个灾难。

“Max ,我们需要让它更好用,你能解决吗”,

“说实话,我宁愿做一个全新的 app”,我笑着说。

“好,按你说的做。那就交给你了,朋友”,

我保持微笑的搓了搓手。终于有机会使用每个人都在讨论的 JS 框架:Vue.js 了。

------------------------------- 我是正文开始的分割线 ------------------------------

我刚写完上述的 app 而且我爱死它了。

由于最近经历的启发,我花了些时间为社区制作了一个 Vue.js 的教程。现在在这里我将主要讲两方面的主题:

  1. 如何用 Vue.js 构建一个简洁的网页 app

  2. 怎样使用 prerender-spa-plugin 做到 SEO 和提前渲染

具体来说,我将带大家创建一个 SEO 友好的简单产品页面。会有现场的 demo 和代码仓库给到大家。

我是通过 our latest headless CMS post 简单的接触到 Vue 的,但这次会是更深入的了解,所以我很兴奋。

我先简单介绍下这个渐进式的框架,因为可能有一些朋友还不了解。

Vue.js 到底是什么?

what-is-vuejs-definition

Vue 是一套帮你构建网页界面的轻量的渐进式 JavaScript 框架。

千万别被定义中的 “JS 框架” 欺骗了。Vue 和跟它对应的流行框架-- React.jsAngular.js 是非常不同的。对初学者来说,它不是 Google 和 Facebook 那样的商业巨头所有的开源产品。

尤雨溪在 2014 年第一次发布 Vue,目的是创造一个自底向上增量式的现代 JS 库。这是 Vue 最强大的特性之一:构建可插入的组件并可以在项目不重构的情况下添加。任何开发者都可以在项目中尝试 Vue 而不会对现有代码产生危害或负担。

模式和专业术语先抛开,我认为 Vue 的前提是:

1. 在一开始你不能知道 app 的全部状态架构

2. 你的数据肯定会在运行时改变

这些限制塑造了库本身:渐进式、基于组件和响应式。这种颗粒式的架构组件让你很容易在保持复用性的同时分离逻辑概念。从顶层来看,它通过原生方法与视图绑定数据,所以可以在必要的时候魔法般的更新(通过观察者)。尽管相同的定义会在很多响应式的前端框架里出现,我发现 Vue 更加优雅的实现了,而且对于我大多数的需求,它是更好的做法。

Vue 的学习曲线相比 React 更平稳,React 需要 JSX 模板知识。甚至有人说 Vue 是去掉了棘手部分的 React 。

最后,Vue 提供的 performance & insightful 开发工具给予了很棒的开发体验。难怪选择 Vue 的开发者像火箭式地上升

vuejs-popularity

从开源项目( Laravel 和 PageKit )到企业( Gitlab 和 Codeship )(别提阿里巴巴和百度),非常多的组织在使用 Vue 。

现在,是时候看看我们要如何使用它。

我们的 Vue.js 例子:一个快速、SEO 友好的电子贸易 app

这个章节我将展示如何使用 Vue 2.0 和 Snipcart 构建一个简单的电子贸易 app ,开发人员编写购物平台的 HTML/JS 。我们同样可以看到如何让爬虫可以爬到产品页面。

预备知识

如果你想深入了解 Vue 2.0 的一切,请点击这里查看系列教程

1. 搭建开发环境

首先使用 vue-cli 生成一个基本的 Vue app 。在终端输入:

npm install -g vue-cli
vue init webpack-simple vue-snipcart

上面的命令将会新建一个名为 vue-snipcart 的新目录,里面包含使用 vue-loader 的基本配置。它也让我们可以编写单文件组件( template/js/css 在同一文件中 )。

我们希望这个示例更加像真实项目,所以我们添加两个在大型 Vue 单页应用中广泛使用的模块:vuexvue-router

  • vuex 是类似 flux 的状态管理模块—非常轻量但强大。它深受 Redux 的影响,你可以前往这里学习

  • vue-router 让你定义路由来动态的导航到对应的组件。

进入 vue-snipcart 目录然后运行如下命令安装它们:

cd vue-snipcart
npm install --save vue-router vuex

另一个要安装的就是 prerender-spa-plugin,它可以让我们提前渲染爬虫需要的路由。

npm install --save prerender-spa-plugin

我们最后装以下四个包就可以了:

  • pug — 用作模板,相比于 HTML 我更喜欢它。

  • vuex-router-sync — 将一些路由信息直接注入 vuex 的 state 里。

  • copy-webpack-plugin — 让我们可以简单的将 static 目录复制到 dist 目录下。

  • babel-polyfill — 使 Vue 运行在 PhantomJS 内部(prerender-spa-plugin 要用到)。

运行下面命令:

npm install --save pug vuex-router-sync copy-webpack-plugin babel-polyfill

2. 配置架构

安装依赖:完成。接下来是配置使我们可以处理存储(store)的数据。

我们先从 vuex store 开始,用它来存储/获取产品信息。

在这个例子中,我们使用静态数据,当然如果我们从服务器获取(fetch)一样也是可以工作的。

注意:在 snipcart 里,我们用一个 JS 基本片段注入 cart ,使用简单 HTML 属性标记定义 products 。

2.1 构造 store

在 src 目录下新建 store 文件夹并在文件夹里新建以下三个文件:

  • state.js 用来定义静态产品

  • getter.js 用来定义一个通过 ID 来检索产品的 get 函数

  • index.js 用来连接上面两个

//state.js
export const state = {
    products: [
        {
            id: 1,
            name: 'The Square Pair',
            price: 100.00,
            description: 'Bold & solid.',
            image: 'https://snipcart.com/media/10171/glasses1.jpeg'
        },
        {
            id: 2,
            name: 'The Hip Pair',
            price: 110.00,
            description: 'Stylish & fancy.',
            image: 'https://snipcart.com/media/10172/glasses2.jpeg'
        },
        {
            id: 3,
            name: 'The Science Pair',
            price: 30,
            description: 'Discreet & lightweight.',
            image: 'https://snipcart.com/media/10173/glasses3.jpeg'
        }
    ]
}

//getters.js
    export const getters = {
        getProductById: (state, getters) => (id) => {
            return state.products.find(product => product.id == id)
        }
    }

//index.js
import Vue from 'vue'
import Vuex from 'vuex'
import { state } from './state.js'
import { getters } from './getters.js'

Vue.use(Vuex)

export default new Vuex.Store({
  state,
  getters
})

2.2 定义路由

我们保持基本的 store:首页列表展示产品 + 每个产品的详情页。所以我们需要注册两个路由。

import VueRouter from 'vue-router'
import Vue from 'vue'
import ProductDetails from './../components/productdetails.vue'
import Home from './../components/home.vue'

Vue.use(VueRouter)

export default new VueRouter({
  mode: 'history',
  routes: [
    { path: '/products/:id', component: ProductDetails },
    { path: '/', component: Home },
  ]
})

我们还没有创建上面的组件,别担心,我们就快了。

需要注意的是我们在 VueRouter 中使用的是 history 模式。这非常重要,不然我们的 prerender-spa-plugin 插件将不起作用。在这种模式下,router 将使用 history API 而不是 hash 来导航。

2.3 把一切连接在一起

现在我们已经有了 store 和 router ,我们需要把它们注册到 app,打开 src/main.js ,并将内容修改为:

import Vue from 'vue'
import App from './App.vue'
import router from './router'
import { sync } from 'vuex-router-sync'
import store from './store'

sync(store, router)

new Vue({
  store,
  router,
  render: h => h(App)
}).$mount('#app')

很简单,不是吗?提前说一下,vuex-router-sync 里的 sync 方法会注入在我们 store state 中一些当前路由的信息,我们一会将会用到。

3. 制作 Vue 组件

有了数据感觉棒极了,但把它显示出来就更棒了。我们将用三个组件来达到显示的目的:

  • Home 用来显示产品列表

  • Product 用来展示 Home 组件里的每个产品

  • ProductDetails 用来显示产品详情

上述文件都新建在 src/components 目录下。

//Home.vue

<template lang="pug">
    div(class="products")
        div(v-for="product in products", class="product")
            product(:product="product")
</template>

<script>
import Product from './../components/Product.vue'

export default {
  name: 'home',
  components: { Product },
  computed: {
    products(){
      return this.$store.state.products
    }
  }
}
</script>

在上面的文件里,我们用 store.state 来获取产品并迭代它们来渲染各自的 Product 组件。

//Product.vue
<template lang="pug">
  div(class="product")
   router-link(v-bind:to="url").product
      img(v-bind:src="product.image" v-bind:alt="product.name" class="thumbnail" height="200")
      p {{ product.name }}
    
    button(class="snipcart-add-item"
      v-bind:data-item-name="product.name"
      v-bind:data-item-id="product.id"
      v-bind:data-item-image="product.image"
      data-item-url="/"
      v-bind:data-item-price="product.price")
        | Buy it for {{ product.price }}$
 
</template>

<script>
export default {
  name: 'Product',
  props: ['product'],
  computed: {
    url(){
      return `/products/${this.product.id}`
    }
  }
}
</script>

我们为每个产品添加链接,通过路由(router)将我们导航到最后的组件。

//ProductDetails.vue
<template lang="pug">
  div(class="product-details")
    
    img(v-bind:src="product.image" v-bind:alt="product.name" class="thumbnail" height="200")
     
    div(class="product-description" v-bind:href="url")
      p {{ product.name }}
      p {{ product. description}}

      button(class="snipcart-add-item"
        v-bind:data-item-name="product.name"
        v-bind:data-item-id="product.id"
        v-bind:data-item-image="product.image"
        data-item-url="/"
        v-bind:data-item-price="product.price")
          | Buy it for {{ product.price }}$

</template>

<script>
export default {
  name: 'ProductDetails',
  computed: {
    id(){
      return this.$store.state.route.params.id
    },
    product(){
      return this.$store.getters.getProductById(this.id)
    }
  }
}
</script>

这个组件的逻辑比上面两个的稍微复杂点。我们从 route 中得到产品的 ID ,然后再通过之前定义的 getter 函数获取对应的产品信息。

4. 创建 app

让我们开始使用新组件吧。

打开 App.vue 文件,里面的内容依然还是一开始通过 vue init webpack-simple 生成的。

将内容替换成以下所示:

<template lang="pug">
  div(id="app")
    TopContext
    router-view

</template>

<script>
import TopContext from './components/TopContext.vue'

export default {
  name: 'app',
  components: { TopContext }
}
</script>

TopContext 组件不是很重要,它只是用来显示头部。关键的是 router-view:它将会由 VueRouter 动态决定,我们之前定义的相关组件将会被注入来替代 router-view

最后要更新的是 index.html ,为了我们的需求,我们在 src 目录下新建 static 文件夹并将 index.html 移到该文件夹下并将内容更新为如下所示:

<!DOCTYPE html><html lang="en">
  <head>
    <meta charset="utf-8">
    <title>vue-snipcart</title>
  </head>

  <body>
  
    <div id="app">    
    </div>
  
    <script src="/build.js"></script>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.2.2/jquery.min.js"></script>
    <script src="https://cdn.snipcart.com/scripts/2.0/snipcart.js" data-api-key="YjdiNWIyOTUtZTIyMy00MWMwLTkwNDUtMzI1M2M2NTgxYjE0" id="snipcart"></script>
    <link href="https://cdn.snipcart.com/themes/2.0/base/snipcart.min.css" rel="stylesheet" type="text/css" />
  </body>
</html>

可以看到我们添加了必要的 Snipcart 脚本。用一个小组件来包含它们可能会更简洁,但因为所有的视图都需要用到,我们选择这样做。

5. 用 Prerender plugin 来实现 Vue.js SEO

vuejs-seo-prerendering

我们 app 的所有内容都是由 JS 动态生成的,是不支持 SEO的:页面中异步内容不能很好的被爬虫抓取,错过这些自然流露对我们的电商网站是非常不明智的。

让我们使用 prerenderingVue.js app 带来更多的 SEO 机会。

与 Vue SSR(Server-Side Rendering)(服务端渲染)相比 ,prerendering 实现更简单且直接。前者更容易使用过度,除非你处理的是非常多的路由,另外,两者都可以达到基本相同的 SEO 结果。

Prerendering 允许我们保持前端为快速、轻量的静态站,它更容易爬取。

我们看一下怎么使用它,打开 webpack.config.js 文件在 export 里添加:

plugins: [
  new CopyWebpackPlugin([{
    from: 'src/static'
  }]),
  new PrerenderSpaPlugin(
    path.join(__dirname, 'dist'),
    [ '/', '/products/1', '/products/2', '/products/3']
  )
]

好了,那它是怎样工作的?

CopyWebpackPlugin 会复制 static 目录(仅仅包含指向 Vue App 的视图)到 dist 目录下。然后 PrerenderSpaPlugin 会调用 PhantomJS 来下载页面的内容并将结果当做静态文件。

哇!我们的 Vue app 已经可以提前渲染和 SEO 友好的产品页面。

你可以运行以下命令自己测试:

npm run build

它将会生成生产环境下我们需要的一切。

其他重要的 SEO 考量

  1. 考虑添加适当的 meta 标签和 app 页面的站点图。你可以从这个学到更多关于“ postProcessHtml ” 的内容

  2. 友好的内容在 SEO 中扮演着重要角色。建议 app 的内容能容易的创建、编辑和优化。为了使编辑器更强大,考虑将无头绪的 CMS 组合构建真正的 JAMstack 。

  3. 使用 HTTPS 连接在 Google 占头等因素。我们在 Netlify 部署这个demo,它提供免费的 SSL 认证。

  4. Mobile-first indexing移动端友好是非常重要的因素,确保移动端的体验跟桌面端一样快速和完整。

Github 仓库和现场 Vue demo

vuejs-tutorial-live-demo

点击下方的链接查看 demo 和源码仓库:

Github 仓库
Vue.js demo

总结

我之前使用过 Vue ,因此制作这个教程比较顺利。我肯定花了一个小时在这个 demo 。使 CopyWebpackPlugin 插件工作让我费了不少劲,好在最后在它的文档里找到了答案。

我希望这篇文章可以鼓励开发者在一些项目中开始使用 Vue ,就像我说的,你可以开始慢慢地用它在现有的项目中开发很小的部分。我觉得这绝对值得一试。我正在开发上面这个,我的领导正在开发商家面板的最新特性,而且他非常喜欢 Vue。另外,当你配置正确后,一个 Vue app 可以实现好的 SEO 结果。

如果你觉得被鼓舞了,查看 the Vue.js Awesome list ,里面有大量的 Vue 示例和项目。

如果你最后深入理解了 Vue ,买件纪念T恤或者赞助下作者

PS:我们将尝试让尤雨溪用 Snipcart 来销售 Vue T恤,但不保证能办到。我们知道 Threadless 在T恤方面也是非常棒的。

【译】JavaScript 核心(第二版)

原文:JavaScript. The Core: 2nd Edition
作者:Dmitry Soshnikov

文章其他语言版本:俄语

这篇文章是 JavaScript. The Core 演讲的第二版,文章内容专注于 ECMAScript 编程语言和其运行时系统的核心组件。

面向读者: 有经验的开发者、专家

文章第一版 涵盖了 JS 语言通用的方面,该文章描述的抽象大多来自古老的 ES3 规范,也引用了一些 ES5 和 ES6( ES2015 )的变更。

从 ES2015 开始,规范更改了一些核心组件的描述和结构,引入了新的模型等等。所以这篇文章我将聚焦新的抽象,更新的术语和在规范版本更替中仍然维护并保持一致的非常基本的 JS 结构。

文章涵盖 ES2017+ 运行时系统的内容。

注释: 最新 ECMAScript 规范 版本可以在 TC-39 网站上查看。

我将从对象的概念开始讲起,它是 ECMAScript 的根本。

对象

ECMAScript 是一门面向对象、基于原型进行组织的编程语言,且它的核心抽象为对象的概念。

定义1:对象: 对象是属性的集合并且有一个原型(prototype)对象。原型的值为一个对象或 null

我们来看一个基本的对象示例。对象的原型可通过内部的 [[Prototype]] 属性引用,在用户代码层面则是暴露在 __proto__ 属性上。

代码如下:

let point = {
  x: 10,
  y: 20,
};

上面的对象有两个显式的属性和一个隐藏的 __proto__ 属性,它是 point 对象的原型引用:

1

注: 对象也可能存储 symbol 。阅读这篇文章了解更多关于 symbol 的内容。

原型对象用于实现动态分配机制的继承。我们先思考一下原型链概念,以便详细了解这个机制。

原型

所有对象在创建的时候都会得到原型。如果没有显式地设置原型,那么对象接收默认原型作为它们的继承对象。

定义2:原型: 原型是一个代理对象,用来实现基于原型的继承。

原型可以通过 __proto__ 属性或 Object.create 方法显式的设置。

// Base object.
let point = {
  x: 10,
  y: 20,
};
 
// Inherit from `point` object.
let point3D = {
  z: 30,
  __proto__: point,
};
 
console.log(
  point3D.x, // 10, inherited
  point3D.y, // 20, inherited
  point3D.z  // 30, own
);

注: 默认情况下,对象接收 Object.prototype 作为它们的继承对象。

任何对象都可作为其它对象的原型,且原型本身可以有原型。如果对象的原型不为 null ,原型的原型不为 null ,以此类推,这就叫做原型链。

定义3:原型链: 原型链是对象的有限链接,用来实现继承和共享属性。

2

规则非常简单:如果对象自身没有一个属性,就会试图在原型上解析属性,然后原型的原型,直到查找完整个原型链。

技术上来说这个机制被称为动态分配或代理。

定义4:代理: 一个在继承链上解析属性的机制。这个过程是在运行时发生的,因此也被叫做动态分配

注: 与此相反的静态分配是在编译的时候解析引用的,动态分配则是在运行时。

如果属性最终都没有在原型链上找到的话,那么返回 undefined 值。

// An "empty" object.
let empty = {};
 
console.log(
 
  // function, from default prototype
  empty.toString,
   
  // undefined
  empty.x,
 
);

从上面的代码可以知道,一个默认的对象实际上永远不为空--它总是从 Object.prototype 继承一些东西。如果想要创建一个无原型的字典(dictionary),我们必须明确地将原型设为 null

// Doesn't inherit from anything.
let dict = Object.create(null);
 
console.log(dict.toString); // undefined

动态分配机制允许继承链完全可变,提供修改代理对象的能力:

let protoA = {x: 10};
let protoB = {x: 20};
 
// Same as `let objectC = {__proto__: protoA};`:
let objectC = Object.create(protoA);
console.log(objectC.x); // 10
 
// Change the delegate:
Object.setPrototypeOf(objectC, protoB);
console.log(objectC.x); // 20

注: 即使 __proto__ 现在是标准属性,并且在解释时使用易于理解,但实践时倾向使用操作原型的 API 方法,如 Object.createObject.getPrototypeOfObject.setPrototypeOf ,类似于反射(Reflect)模块。

从上面 Object.prototype 示例我们知道同一个原型可以给多个对象共享。从这个原理出发,ECMAScript 实现了基于类的继承。我们看下示例,并且深入了解 JS 的 “类(class)” 抽象。

当多个对象共享同一个初始的状态和行为时,它们就形成了一个

定义5:类: 一个类是一个正式的抽象集,它规定了对象的初始状态和行为。

假如我们需要多个对象继承同一个原型,我们当然可以创建这个原型并显式的继承它:

// Generic prototype for all letters.
let letter = {
  getNumber() {
    return this.number;
  }
};
 
let a = {number: 1, __proto__: letter};
let b = {number: 2, __proto__: letter};
// ...
let z = {number: 26, __proto__: letter};
 
console.log(
  a.getNumber(), // 1
  b.getNumber(), // 2
  z.getNumber(), // 26
);

我们可以从下图看到这些关系:

3

然而这明显很繁琐。类抽象正是服务这个目的 - 作为一个语法糖(和构造器在语义上所做的一样,但是是更友好的语法形式),它让我们使用更方便的模式创建那些对象:

class Letter {
  constructor(number) {
    this.number = number;
  }
 
  getNumber() {
    return this.number;
  }
}
 
let a = new Letter(1);
let b = new Letter(2);
// ...
let z = new Letter(26);
 
console.log(
  a.getNumber(), // 1
  b.getNumber(), // 2
  z.getNumber(), // 26
);

注: ECMAScript 中基于类的继承是在基于原型的代理之上实现的。

注: 一个“类”只是理论上的抽象。技术上来说,它可以像 Java 或 C++ 一样通过静态分配来实现,也可以像 JavaScript、Python、Ruby 一样通过动态分配(代理)来实现。

技术上来说一个“类”表示“构造函数 + 原型”的组合。因此构造函数创建对象并自动设置新创建实例的原型。这个原型存储在 <ConstructorFunction>.prototype 属性上。

定义6:构造器: 构造器是一个函数,它用来创建实例并自动设置它们的原型。

我们可以显式的使用构造函数。此外,在类抽象引入之前,JS 开发者过去因为没有更好的选择而这样做(我们依然可以在互联网上找到大量这样的遗留代码):

function Letter(number) {
  this.number = number;
}
 
Letter.prototype.getNumber = function() {
  return this.number;
};
 
let a = new Letter(1);
let b = new Letter(2);
// ...
let z = new Letter(26);
 
console.log(
  a.getNumber(), // 1
  b.getNumber(), // 2
  z.getNumber(), // 26
);

创建单级的构造函数非常简单,而从父类继承的模式则要求更多的模板代码。目前这些模板代码作为实现细节被隐藏,而这正是在我们创建 JavaScript 类时在底层所发生的。

注: 构造函数就是类继承的实现细节。

我们看一下对象和它们的类的关系:

4

上图显示了每个对象都有一个关联的原型。就连构造函数(类)也有原型也就是 Function.prototype 。我们看到 a、b 和 c 是 Letter 的实例,它们的原型是 Letter.prototype

注: 所有对象的实际原型总是 __proto__ 引用。构造函数显式声明的 prototype 属性只是一个指向它实例的原型的引用;实例的原型仍然是通过 __proto__ 引用得到。点此链接详细了解

你可以在文章 ES3. 7.1 OOP: The general theory 中找到关于 OPP 通用概念(包括基于类、基于原型等的详细介绍)的详细讨论。

现在我们已经了解了 ECMAScript 对象间的基本关系,让我们更深入的了解 JS 运行时系统。我们将会看到,几乎所有的东西都可以用对象表示。

执行上下文

为了执行 JS 代码并追踪其运行时的计算,ECMAScript 规范定义了 执行上下文(execution context) 的概念。逻辑上执行上下文是用 来保持的(执行上下文栈我们一会就会看到),它与 调用栈(call stack) 的通用概念相对应。

定义7:执行上下文: 执行上下文是一个规范策略,用于追踪代码的运行时计算。

ECMAScript 代码有几种类型:全局代码、函数和 eval ;它们都在各自的执行上下文中运行。不同的代码类型及其适当的对象可能会影响执行上下文的结构:例如,生成器函数(generator functions) 会将其 生成器对象(generator object) 保存在上下文中。

我们看一个递归函数调用:

function recursive(flag) {
 
  // Exit condition.
  if (flag === 2) {
    return;
  }
 
  // Call recursively.
  recursive(++flag);
}
 
// Go.
recursive(0);

当一个函数调用时,就创建一个新的执行上下文并把它压入栈 - 这时它就成了活跃的执行上下文。当函数返回时,其上下文就从栈中推出。

我们将调用另一个上下文的上下文称为调用者(caller)。被调用的上下文因此就叫做被调用者(callee)。在上面的例子中,recursive 函数同时承担着上述两者角色:调用者和被调用者 - 当递归地调用自身。

定义8:执行上下文栈: 执行上下文栈是一个后进先出的结构,它用来维护控制流和执行顺序。

在上面的例子中,我们对栈有“压入-推出”的修改:

5

我们可以看到,全局上下文一直都在栈的底部,它是在执行任何其他上下文之前创建的。

你可以在这篇文章中找到更多关于执行上下文的详细内容。

一般情况下,一个上下文中的代码会运行到结束,然而正如我们上面所提到的,一些对象 - 如生成器,可能会违反栈后进先出的顺序。一个生成器函数可能会挂起其运行上下文并在完成之前将其从栈中移除。当生成器再次激活时,其上下文恢复并再次被压入栈:

function *gen() {
  yield 1;
  return 2;
}
 
let g = gen();
 
console.log(
  g.next().value, // 1
  g.next().value, // 2
);

上面代码中的 yield 语句返回值给调用者并将上下文推出。第二次调用 next 时,相同的上下文再次被压入栈并恢复。这样的上下文会比创建它的调用者生命周期更长,因此违反了后进先出的结构。

注: 你可以阅读这篇文档了解关于生成器和迭代器的更多内容。

现在我们将讨论执行上下文的重要组成部分;特别是 ECMAScript 运行时如何管理变量的存储和代码中嵌套块创建的作用域(scope)。这是 词法环境(lexical environments) 的通用概念,它在 JS 中用来存储数据和解决“ 函数参数问题(Funarg problem) ” - 和 闭包(closure) 的机制一起。

环境

每个执行上下文都有一个相关的词法环境

定义9:词法环境: 词法环境是用于定义上下文中出现的标识符与其值之间的关联的结构。每个环境都可以有一个指向其可选父环境的引用。

所以,一个环境是在某个范围内定义的变量,函数和类的存储

从技术上来说,一个环境是由一个环境记录(一个将标识符映射到值的实际存储表)和一个对父项(可能是 null)的引用这一对组成。

看代码:

let x = 10;
let y = 20;
 
function foo(z) {
  let x = 100;
  return x + y + z;
}
 
foo(30); // 150

上面代码的全局上下文foo 函数的上下文的环境结构如下图所示:

6

从逻辑上讲,这使我们想起上面讨论过的原型链。并且标识符解析的规则也非常相似:如果在自己的环境中找不到变量,则尝试在父级环境中、在父级父级中查找它,以此类推 - 直到整个环境链都查找完成。

定义10:标识符解析: 在环境链中解析变量(绑定)的过程。 无法解析的绑定会导致 ReferenceError

这就解释了:为什么变量 x 被解析为 100,而不是 10 - 它是直接在 foo 自己的环境中找到;为什么我们可以访问参数 z - 它也只是存储在激活环境中;也是为什么我们可以访问变量 y - 它是在父级环境中找到的。

与原型类似,相同的父级环境可以由多个子环境共享:例如,两个全局函数共享相同的全局环境。

注: 您可以在这篇文章中获得有关词法环境的详细信息。

环境记录因类型而异。有对象环境记录和声明式环境记录。在声明式记录之上还有函数环境记录和模块环境记录。每种类型的记录都有它的特性。但是,标识符解析的通用机制在所有环境中都是通用的,并且不依赖于记录的类型。

一个对象环境记录的例子就是全局环境记录。这种记录也有相关联的绑定对象,它可以存储记录中的一些属性,而不是全部,反之亦然(译者注:不同的可以看下面的示例代码)。绑定对象也可以通过 this 得到。

// Legacy variables using `var`.
var x = 10;
 
// Modern variables using `let`.
let y = 20;
 
// Both are added to the environment record:
console.log(
  x, // 10
  y, // 20
);
 
// But only `x` is added to the "binding object".
// The binding object of the global environment
// is the global object, and equals to `this`:
 
console.log(
  this.x, // 10
  this.y, // undefined!
);
 
// Binding object can store a name which is not
// added to the environment record, since it's
// not a valid identifier:
 
this['not valid ID'] = 30;
 **加粗文字**
console.log(
  this['not valid ID'], // 30
);

上述代码可以表示为下图:

7

需要注意的是,绑定对象的存在是为了兼容遗留的结构,例如 var 声明和with 语句,它们也将它们的对象作为绑定对象提供。这就是环境被表示为简单对象的历史原因。现在,环境模型更加优化,但其结果是,我们无法再将绑定作为属性访问(译者注:如上面的代码中我们不能通过 this.y 访问 y 的值)。

我们已经看到环境是如何通过父链接相关联的。现在我们将看到一个环境的生命周期如何比创造它的上下文环境的更久。这是我们即将讨论的闭包机制的基础。

闭包

ECMAScript中的函数是头等的(first-class)。这个概念是函数式编程的基础,这些方面也被 JavaScript 所支持。

定义11:头等函数: 它是一种函数,其可以作为正常数据参与:存储在变量中,作为参数传递,或作为另一个函数的值返回。

与头等函数概念相关的是所谓的“函参问题(Funarg problem)”(或“一个函数参数的问题”)。当一个函数需要处理自由变量时,这个问题就会出现。

定义12:自由变量: 一个既不是参数也不是自身函数的局部变量的变量。

我们来看看函参问题,并看它在 ECMAScript 中是如何解决的。

考虑下面的代码片段:

let x = 10;
 
function foo() {
  console.log(x);
}
 
function bar(funArg) {
  let x = 20;
  funArg(); // 10, not 20!
}
 
// Pass `foo` as an argument to `bar`.
bar(foo);

对于函数 foo 来说,x 是自由变量。当 foo 函数被激活时(通过
funArg 参数) - 应该在哪里解析 x 的绑定?是创建函数的外部作用域还是调用函数的调用者作用域?正如我们所见,调用者即 bar 函数,也提供了 x 的绑定 - 值为 20 。

上面描述的用例被称为 downward funarg problem,即在确定绑定的正确环境时的模糊性:它应该是创建时的环境,还是调用时的环境?

这是通过使用静态作用域的协议来解决的,也就是创建时的作用域。

定义13:静态作用域: 一种实现静态作用域的语言,仅仅通过查看源码就可以确定在哪个环境中解析绑定。

静态作用域有时也被称为词法作用域,因此也就是词法环境的命名由来。

从技术上来说,静态作用域是通过捕获创建函数的环境来实现的。

注: 您可以阅读链接文章的了解静态和动态作用域。

在我们的例子中,foo 函数捕获的环境是全局环境:

8

我们可以看到一个环境引用了一个函数,而这个函数又回引了环境。

定义14:闭包: 闭包是捕获定义环境的函数。在将来此环境用于标识符解析。

注: 一个函数调用时是在全新的环境中激活,该环境存储局部变量参数。激活环境的父环境被设置为函数的闭包环境,从而产生词法作用域语义。

函参问题的第二个子类型被称为 upward funarg problem 。它们之间唯一的区别是捕捉环境的生命周期比创建它的环境更长。

我们看例子:

function foo() {
  let x = 10;
   
  // Closure, capturing environment of `foo`.
  function bar() {
    return x;
  }
 
  // Upward funarg.
  return bar;
}
 
let x = 20;
 
// Call to `foo` returns `bar` closure.
let bar = foo();
 
bar(); // 10, not 20!

同样,从技术上来说,它与捕获定义环境的确切机制没有区别。只是这种情况下,如果没有闭包,foo 的激活环境就会被销毁。但是我们捕获了它,所以它不能被释放,并被保留 - 以支持静态作用域语义。

人们对闭包的理解通常是不完整的 - 开发人员通常考虑闭包仅仅依据 upward funarg problem(实际上是更合理)。但是,正如我们所看到的,downwardupward funarg problem 的技术机制是完全一样的 - 就是静态作用域的机制。

正如我们上面提到的,与原型类似,几个闭包可以共享相同的父环境。这允许它们访问和修改共享数据:

function createCounter() {
  let count = 0;
 
  return {
    increment() { count++; return count; },
    decrement() { count--; return count; },
  };
}
 
let counter = createCounter();
 
console.log(
  counter.increment(), // 1
  counter.decrement(), // 0
  counter.increment(), // 1
);

由于在包含 count 变量的作用域内创建了两个闭包:incrementdecrement ,所以它们共享这个父作用域。也就是说,捕获总是“通过引用” 发生 - 意味着对整个父环境的引用被存储。

9

有些语言可能捕获的是值,制作捕获的变量的副本,并且不允许在父作用域中更改它。但是,重复一遍,在 JS 中,它始终是对父范围的引用。

注: 引擎的实现可能会优化这一步,而不会捕获整个环境。只捕获使用的自由变量,但它们仍然在父作用域中保持不变的可变数据。

你可以在链接文章中找到有关闭包和函参问题的详细讨论。

所有的标识符都是静态的作用域。然而,在 ECMAScript 中有一个值是动态作用域的。那就是 this 的值。

this

this 值是一个特殊的对象,它是动态地、隐式地传递给上下文中的代码。我们可以把它看作是一个隐含的额外参数,我们可以访问,但是不能修改。

this 值的目的是为多个对象执行相同的代码。

定义15:this: 一个隐式的上下文对象,可以从一个执行上下文的代码中访问 - 以便为多个对象执行相同的代码。

this 主要的用例是基于类的 OOP。一个实例方法(在原型上定义)存在于一个范例中,但在该类的所有实例**享。

class Point {
  constructor(x, y) {
    this._x = x;
    this._y = y;
  }
 
  getX() {
    return this._x;
  }
 
  getY() {
    return this._y;
  }
}
 
let p1 = new Point(1, 2);
let p2 = new Point(3, 4);
 
// Can access `getX`, and `getY` from
// both instances (they are passed as `this`).
 
console.log(
  p1.getX(), // 1
  p2.getX(), // 3
);

getX 方法被激活时,会创建一个新的环境来存储局部变量和参数。另外,函数环境记录得到传递来的 [[ThisValue]] ,它是根据函数的调用方式动态绑定的。当用 p1 调用时,this 值恰好是 p1 ,第二种情况下是 p2

this 的另一个应用是泛型接口函数,它可以用在 mixintraits 中。

在下面的例子中,Movable 接口包含泛型函数 move ,它期望这个 mixin 的用户实现 _x_y 属性:

// Generic Movable interface (mixin).
let Movable = {
 
  /**
   * This function is generic, and works with any
   * object, which provides `_x`, and `_y` properties,
   * regardless of the class of this object.
   */
  move(x, y) {
    this._x = x;
    this._y = y;
  },
};
 
let p1 = new Point(1, 2);
 
// Make `p1` movable.
Object.assign(p1, Movable);
 
// Can access `move` method.
p1.move(100, 200);
 
console.log(p1.getX()); // 100

作为替代方案,mixin 也可以应用于原型级别,而不是像上例中每个实例做的那样。

为了展示 this 值的动态性,考虑下面例子,我们把这个例子留给读者来解决:

function foo() {
  return this;
}
 
let bar = {
  foo,
 
  baz() {
    return this;
  },
};
 
// `foo`
console.log(
  foo(),       // global or undefined
 
  bar.foo(),   // bar
  (bar.foo)(), // bar
 
  (bar.foo = bar.foo)(), // global
);
 
// `bar.baz`
console.log(bar.baz()); // bar
 
let savedBaz = bar.baz;
console.log(savedBaz()); // global

因为只通过查看 foo 函数的源代码,我们不能知道它在特定的调用中 this 的值是什么,所以我们说 this 值是动态作用域。

注: 您可以在这篇文章中得到关于如何确定 this 值的详细解释,以及为什么上面的代码是那样的结果。

箭头函数this 值比较特殊:其 this 是词法的(静态的),而不是动态的。即他们的函数环境记录不提供 this 值,它是从父环境中获取的。

var x = 10;
 
let foo = {
  x: 20,
 
  // Dynamic `this`.
  bar() {
    return this.x;
  },
 
  // Lexical `this`.
  baz: () => this.x,
 
  qux() {
    // Lexical this within the invocation.
    let arrow = () => this.x;
 
    return arrow();
  },
};
 
console.log(
  foo.bar(), // 20, from `foo`
  foo.baz(), // 10, from global
  foo.qux(), // 20, from `foo` and arrow
);

就像我们所说的,在全局上下文this 值是全局对象(全局环境记录绑定对象)。以前只有一个全局对象。在当前版本的规范中,可能有多个全局对象,这是 代码领域(code realms) 的一部分。我们来讨论一下这个结构。

领域

在求值之前,所有 ECMAScript 代码都必须与一个领域相关联。从技术上来说,一个领域只是为一个上下文提供全局环境。

定义16:领域: 代码领域是封装独立的全局环境的对象。

当一个执行上下文被创建时,它与一个特定的代码领域相关联,这个代码领域为这个上下文提供了全局环境。该关联在未来将保持不变。

注: 浏览器环境中的直接领域是 iframe 元素,正是它提供了一个自定义的全局环境。在 Node.js 中,它和 vm 模块的沙箱类似。

规范的当前版本没有提供显式创建领域的能力,但是它们可以由实现隐含地创建。不过有一个将这个API暴露给用户代码的提案

从逻辑上来说,堆栈中的每个上下文总是与其领域相关联:

10

现在我们正在接近 ECMAScript 运行时的全貌了。然而,我们仍然需要看到代码的入口点和初始化过程。这是由 jobs(作业)job queues(作业队列) 机制管理的。

Job

有一些操作可以被推迟的,并在执行上下文堆栈上有可用点时立即执行。

定义17:Job: Job 是一个抽象操作,当没有其他 ECMAScript 计算正在进行时,该操作启动 ECMAScript 计算。

Job 在 作业队列 中排队,在当前的规范版本中有两个作业队列 ScriptJobsPromiseJobs

ScriptJobs 队列中的初始 job 是我们程序的主要入口 - 初始化已加载且求值的脚本:创建一个领域,创建一个全局上下文,并且与这个领域相关联,它被推入堆栈,全局代码被执行。

需要注意的是,ScriptJobs 队列管理着脚本和模块两者。

此外,这个上下文可以执行其他上下文,或使其他 jobs 到队列中排队。一个可以产生排队的 job 就是promise。

如果没有正在运行的执行上下文,并且执行上下文堆栈为空,则 ECMAScript 实现会从作业队列中移除第一个 job,创建执行上下文并开始执行。

注: 作业队列通常由被称为 “事件循环” 的抽象来处理。ECMAScript 标准没有指定事件循环,而是将其留给实现决定,但是你可以在链接页面找到一个教学示例。

示例:

// Enqueue a new promise on the PromiseJobs queue.
new Promise(resolve => setTimeout(() => resolve(10), 0))
  .then(value => console.log(value));
 
// This log is executed earlier, since it's still a
// running context, and job cannot start executing first
console.log(20);
 
// Output: 20, 10

注: 你可以在链接文档中阅读有关 promise 的更多信息。

async 函数 可以等待(await) promise 执行,所以它们也使 promise 作业排队:

async function later() {
  return await Promise.resolve(10);
}
 
(async () => {
  let data = await later();
  console.log(data); // 10
})();
 
// Also happens earlier, since async execution
// is queued on the PromiseJobs queue.
console.log(20);
 
// Output: 20, 10

注: 更多 async 函数内容请点击链接

现在我们已经非常接近当前 JS 宇宙的最终画面。马上我们将看到我们讨论的所有组件的主要所有者 - 代理商(Agents)。

Agent

ECMAScript中的 并发(concurrency)并行(parallelism) 是使用 代理模式(Agent pattern)
的实现的。代理模式非常接近参与者模式(Actor pattern) - 一个具有消息传递风格的轻量级进程。

定义18:Agent: 代理是封装执行上下文堆栈、作业队列集和代码领域的抽象概念。

依赖代理的实现可以在同一个线程上运行,也可以在单独的线程上运行。浏览器环境中的 Worker 代理是代理概念的一个例子。

代理的状态是相互隔离的,可以通过发送消息进行通信。一些数据可以在代理之间共享,例如
SharedArrayBuffer 。代理也可以组合成代理集群

在下面的例子中,index.html 调用 agent-smith.js worker ,传递共享的内存块:

// In the `index.html`:
 
// Shared data between this agent, and another worker.
let sharedHeap = new SharedArrayBuffer(16);
 
// Our view of the data.
let heapArray = new Int32Array(sharedHeap);
 
// Create a new agent (worker).
let agentSmith = new Worker('agent-smith.js');
 
agentSmith.onmessage = (message) => {
  // Agent sends the index of the data it modified.
  let modifiedIndex = message.data;
 
  // Check the data is modified:
  console.log(heapArray[modifiedIndex]); // 100
};
 
// Send the shared data to the agent.
agentSmith.postMessage(sharedHeap);

worker 的代码如下:

// agent-smith.js
 
/**
 * Receive shared array buffer in this worker.
 */
onmessage = (message) => {
  // Worker's view of the shared data.
  let heapArray = new Int32Array(message.data);
 
  let indexToModify = 1;
  heapArray[indexToModify] = 100;
 
  // Send the index as a message back.
  postMessage(indexToModify);
};

你可以在链接页面得到示例的完整代码。

(需要注意的是,如果你在本地运行这个例子,请在 Firefox 中运行它,因为由于安全原因,Chrome 不允许从本地文件加载 web worker)

下图展示了 ECMAScript 运行时:

11

如图所示,那就是在 ECMAScript 引擎下发生的事情!

现在文章到了结尾的时候。这是我们可以在概述文章中涵盖的 JS 核心的信息量。就像我们提到的,JS 代码可以被分组成模块,对象的属性可以被 Proxy 对象追踪等等。 - 许多用户级别的细节可以在 JavaScript 语言的不同文档中找到。

尽管我们试图表示一个 ECMAScript 程序本身的逻辑结构,希望能够澄清这些细节。如果你有任何问题,建议或反馈意见,我将一如既往地乐于在评论中讨论这些问题。

我要感谢 TC-39 的代表和规范编辑帮助澄清本文。该讨论可以在这个 Twitter 主题中找到。

祝学习 ECMAScript 好运!

Written by: Dmitry Soshnikov
Published on: November 14th, 2017

Angular 学习笔记之 Controller

Angular的核心分为三大部分:Controller、Service、Directive。今天来整理下我的controller学习经验。可以说理解了controller基本上就能够解决大部分Angular开发的问题。因为绝大部分的数据都是在controller里操作的,而Angular的核心**就是以数据为中心。

如果大家之前了解过angular,可能听过父controller和子controller,实际上Controller之间是没有继承关系的,但angular有一个非常重要的属性就是$scope(可以简单理解为作用域),$scope是通过原型链继承的。而每个controller实例又可以注入$scope,所以controller之间可以实现作用域继承。但controller和$scope不是等价的,directive里也有$scope,所以controller和directive之间也可以实现作用域继承。所有的$scope都继承自$rootScope。我们来看下面这段代码和结果:

demo1
qq20160708-2

我们可以很明显的看出它们之间的继承关系,parentController里没定义childName属性,所以为空值。下面我们先改变parentController里的parent的值,然后再改变childController里的值,分别对应下面的两张图:

demo2

相信理解原型链继承的朋友对这个结果一下就能想明白,原型对象属性的改变会影响所有实例,实例上定义与原型属性同名的属性会把原型上的覆盖。

这样我们就可以得到第一种在controller之间通信的方式,如下图所示,不过这种方式只能实现父controller向子controller传值。

demo3

提到controller之间的通信,这是controller的一个重点。$scope提供了三个方法进行通信:$broadcast、$emit和$on。$broadcast用来向后代controller播报事件,$emit用来向祖先controller播报事件,$on用来监听事件。上代码:

qq20160709-5

demo4

我们需要一个事件来触发$broadcast和$emit方法,这里我用的是click事件,这两个方法的第一个参数都是事件名(必写),后面的参数都是可选的,写上我们想传递的变量或值。$on方法第一个参数必须与想监听的事件名一致,第二个参数是一个函数(理论上是可选的,但是不写那就没任何意义了),用来监听到事件的后续处理。函数的参数选填,有参数的话:第一个参数默认为event对象,有兴趣了解event对象有那些属性和方法的可以自行log出来,接下来的参数才是我们监听到的事件传递过来的值,一一对应。

接下来我们触发这两个方法,先在parentController的input输入值,点击按钮;再childController的input输入值,分别对应下面两图:

demo5

看图说话,我们的方法是有用的。

这样是不是就能解决我们的所有问题了?很显然不是,要是我想在没有嵌套关系的controller之间通信呢?

相信朋友们已经想到了,$rootScope肯定是有这三个方法的。所以不管我们想用什么姿势通信,$rootScope都可以帮我们解锁。

大家想要深入掌握,一定要把$rootScope对象log出来看一下。

Angular的核心中的核心就是$scope

Angular 学习笔记之 Directive

今天整理一下Angular重点之一的Directive。directive,中文叫指令。大家应该都知道了controller是操作数据的地方,而dom操作应该都放到directive里,当然指令的作用肯定不止是操作dom。

angular的项目时时刻刻都是在跟指令打交道,像常用的也是自带的一些指令如:ng-click、ng-if、ng-repeat等。像这些自带的指令就没什么好说的了,大家移步文档看就行了

我要讲的主要就是自定义指令:举3个例子,简单介绍如何创建自定义的指令。

我们先来一个最简单的例子来了解自定义指令以及指令如何与controller进行交互:用my-click自己实现ng-click。

demo1

首先给button按钮添加my-click属性,重点看一下controller定义的函数和directive方法创建指令。

directive里的restrict属性,它告诉AngularJS这个指令在DOM中以何种形式被声明。有E(元素)、A(属性)、C(类名)、M(注释) 4种方式,restrict的默认值是A,即以属性的形式来进行声明。这里值为A即属性。link方法注入了三个依赖(scope, ele, attr)(link里的scope习惯不加$),scope就不用多说了吧,ele表示directive绑定的元素,attr表示元素上的属性(它是一个对象),想深入了解的请自行log出来。这里最重要的莫过于scope.$apply方法,其实这里可以不需要$apply方法,因为没涉及到数据的更新,但是一般都会这样写,因为扩展print函数的时候就不用改了,$apply方法就是要手动触发angular的脏值检测,简单理解就是更新数据的意思。这个知识点是非常重要的,当我们用angular自带的方法是不需要手动触发脏值检测的,比如 $timeout ,但我们要是用 setTimeout 的话就要去手动触发,不然视图上绑定的值是不会更新的。

我们点击按钮之后看下输出:

qq20160722-0

很明显,这个自定义的指令是work的。

我们来实现一个上传图片的指令file-upload:

demo3

qq20160722-11

这里就用到了更多的directive属性:scope属性表示创建独立作用域(只要写了这个属性,不管值为空对象还是非空),它的值是一个对象,里面可以绑定所在视图controller的字符串、函数及双向数据绑定),想详细了解的请自行google,一定要重点了解一下scope属性的用法。这里的$scope.$apply()就有大用处了,在link里一定要手动触发脏值检测才会更新数据。

我们选择图片后:

qq20160722-12

自定义上传图片的指令就完成了,大家可以试下不加$scope.$apply()看下结果。

再来了解一下directive里用到controller方法的情况。这里我就不贴完整的代码了。

qq20160722-13

controller方法跟link方法有一些区别:不需要手动调$apply方法,会自动更新数据;可以使用我们之前提到的broadcast、emit、on方法进行通信。

最后,提醒大家一点:html里所有的directive和与directive相关的属性的用_连字符 -_ 的写法,如file-data,在directive里用驼峰写法,如fileData。不然的话,会很坑的。

【译】Node 6 升级到 Node 8 :真实应用中的性能比较

原文地址:Upgrading from Node 6 to Node 8: a real-world performance comparison

原文作者:David Gilbertson

Node 8 已经发布了,你听说了没?他们说新版本的速度更快了。

但是没有任何数字的话,更快仅仅只是几个字。

好在我有一个很大的 React 网站运行在 Node 6 上,并且有 2 个小时的空闲。

更新到 Node 8 足够简单 - 只需要十分钟,没有任何的不兼容库。我从这里下载了 macOS 系统的 .pkg 文件,并且安装的很顺利。虽然我需要手动的去删除 usr/local/lib/node_modules/ 目录。

上面进行的很不错,但一会我将会在 Windows 上更新它,估计它将花费 4 天的时间。

PS:我的天!Node 8 在 Windows 上安装的异常成功。没有人工的步骤,没有不兼容的库,没有不久前还需要的乱七八糟的配置

关于网站

下面比较的是一个中等偏大的 React 网站的一个页面的性能。在服务端,它将有一两千个属性的 JSON 对象传递给模板,然后返回包含 2113 个 DOM 节点的 HTML 。

(没错,是有太多的 DOM 节点了。它在手机端整整需要 2 分钟才能解析完,但我处在食物链的底层,并不能做什么改变。而且有一半的节点是隐藏的,仅仅是为了 "SEO" - 甚至让我无法开始)

··············································································································································

我们开始实验吧。

服务端渲染时间

我们从最重要的指标开始,服务器渲染页面所需的时间。

这是在我的大的银色的装有 macOS Sierra 系统的笔记本上测试的,伴随着几 GHz 的嗡嗡声。

2

刚开始的时候,并没有很大的差别,但在第八次的时候,渲染时间趋于稳定了,Node 6 花费了大约 104 ms 完成渲染,而 Node 8 只花了大约 80 ms 。

Node 使渲染时间减少了相当不错的 23 % 。或更具体的说,服务这个网站所需的硬件减少了 23% 。

我将建议我的老板升级到 Node 8 ,并将亚马逊云服务实例从 25 减少到 20 ,然后把第一个月节省的钱捐给 Node.js 基金。

因为我喜欢笑声。(这句话有点迷,原话:Because I enjoy the sound of laughter )

··············································································································································

下面的是同样的测试,但是跑在 React 的测试模式:

3

在运行了几次之后,Node 8 平均减少了大约 31% 。上面这个图表仅仅只是提醒大家将 NODE_ENV 设置成 production 并使用库的生产环境版本是很重要的。

··············································································································································

我不是很确定 Node 8 性能的提高是怎么来的。我认为大部分来自 V8 5.8 。如果你对它怎样工作感兴趣的话,可以看这个很棒的视频

运行一套测试

这一套测试有大约 500 Jest 的测试用例,它们都仅仅是普通的 JavaScript ,大部分调用的都是插入和销毁 React 组件。

4

Node 用时少了 10% 。可能性能的改进远不止上面所表示的,因为 JavaScript 引擎并没有做任何的优化(每一个测试都是新的 Node 进程)。当然前面只是猜测,随时欢迎指正或说明。

Webpack 构建

用 Webpack 构建大约 500 KB JavaScript ,其过程中有不少的磁盘 I/O ,一连串的 Babel 编译和 JS 的压缩混淆。

5

Node 8 减少了 7% 的用时,其它并没有值得说的。

NPM 安装

现在换成 Windows - 最广泛使用 NPM 的操作系统 。

package.json 里有 40 个包;依赖树加起来有 445 个包。

下面第一次测试时,我删除了 npm 缓存和项目目录的 node_modules 文件夹,这样测试时就是从网络上拉取依赖包。

有意思的是 NPM 3 的最快速度是 7 Mbps,而 NPM 5 则达到了 20 Mbps 。

为了增加点乐趣,我还加入了 yarn 作对比。

6

另外对下面这句话的作者说一句:

8

你让我轻笑了起来。但就像 Marcel Marceau 曾经说过,这是一个紧张的笑声,因为我并不知道我在做什么。

··············································································································································

接下来,我在每次运行 npm install 前都先删除 node_modules 文件夹但保留 npm 缓存。我认为这大部分都是磁盘 I/O (从 缓存 复制到项目目录),但很显然不止是复制,因为用时的巨大改善表明了这点。

7

在上面两种情况下,NPM 5 都减少了三分之一的安装用时。并且 Yarn 都显著地更快(在我的需求下不值得切换到 Yarn,但别人就不一定了)。

··············································································································································

朋友们,上面就是我测试的全部图表了。

实话实说,我一开始只是期待 Node 8 可能有几个百分点的性能改进,如果不是在真实项目中测试了,我也不会如此的震惊。能够减少四分之一的服务端渲染时间和三分之一的 NPM 安装用时真的太棒了。

你们做的太棒了,所有的 Node 的贡献者,你们做的真的太棒了。

··············································································································································

当然,Node 8 还有很多新特性。

《算法导论》读书笔记-几种常见的排序算法

《算法导论》前四章介绍了几种简单的排序算法,这里用 js 整理成代码。由于高数没学好,所以关于复杂度的分析实在是看不太懂 😓

首先说明:排序的数据结构都是 js 的数组,index 是从 0 开始到 length - 1

  • 插入排序

插入排序:从 index i = 1 开始,循环与 index < i 比较,将数据插入到合适位置。代码如下:

function insertSort(arr) {
    for (let i = 1; i < arr.length; i++) {
       let j = i - 1;
       while (j >= 0 && arr[j + 1] < arr[j]) {
          [arr[j + 1], arr[j]] = [arr[j], arr[j + 1]];
          j--;
       }
    }
    return arr;
}
  • 冒泡排序

冒泡排序:反复比较相邻的两个数,如果左边大于右边,则交换位置。代码如下:

function bubbleSort(arr) {
    let length = arr.length;
    for (let i = 0; i < length; i++) {
       for (let j = i; j < length - 1 - i; j++) {
          if (arr[j] > arr[j + 1]) {
             [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
          }
       }
    }
    return arr;
}
  • 选择排序

选择排序:先将数组中最小的数交换到 arr[0],再将第二小的数交换到 arr[1],如此反复。代码如下:

function selectSort(arr) {
    let length = arr.length;
    let currentMin, currentMinIndex;
    for (let i = 0; i < length - 1; i++) {
       currentMin = arr[i];
       currentMinIndex = i;
       for (let j = i; j < length; j++) {
          if (currentMin > arr[j]) {
             currentMin = arr[j];
             currentMinIndex = j;
          }
          if (j === length - 1 && currentMinIndex !== i) {
             [arr[i], arr[currentMinIndex]] = [arr[currentMinIndex], arr[i]]
          }
       }
    }
    return arr;
}
  • 归并排序

归并排序:将原数组递归拆分成长度为一的最小数组,再两两组合排序,生成最终顺序数组(觉得讲的不是很清楚,结合书上章节及插图会好理解的多)。代码如下:

function mergeSort(arr) {
    let len = arr.length
    if (len < 2) {
      return arr
    }
    let middle = Math.floor(len / 2),
      left = arr.slice(0, middle),
      right = arr.slice(middle)
      
    return merge(mergeSort(left), mergeSort(right))
}

function merge(left, right) {
    let result = []

    while (left.length && right.length) {
      if (left[0] <= right[0]) {
        result.push(left.shift())
      } else {
        result.push(right.shift())
      }
    }

    while (left.length) {
      result.push(left.shift())
    }

    while(right.length) {
      result.push(right.shift())
    }

    return result
}

vue2 实践小结

vue2出来也一段时间了,各方面的反响都挺好的,作为一个一直在用 ng1 的前端,必须找时间熟悉一下大热的 vue2。虽然工作上还在用 ng1,但我自己前段时间闲暇时也做了个 ng2 的 demo 项目。有了 ng1比较足的经验和 ng2 的小试牛刀,vue2 对于我可以说是上手毫无难度的。

我选的是 github 的 api 来提供数据, vue2 做前端展示。效果点击查看源码,欢迎各位大大拍砖。

qq20170110-0

其实也只是简单的尝试了下 vue2,并没有用到很深的东西,用到的全家桶也只有 vue-router,当然也因为 vue-resource 被尤大抛弃了,不过确实不好用,所以我选的是 axios 。

为了快速起步,肯定选 vue-cli 脚手架,按照示例很简单就能跑起来了。下面遇到的问题基本都是文档写的不详细导致的,这不是故意黑,对比 ng2 的文档,确实差点意思。

vue-router

当我把 vue-router 引入之后,突然发现不知道怎么启动。因为文档的例子是报错的,应该是不兼容用 vue-cli 下的项目,所以我去 github 搜了好几个 rep 才找到正确的写法,因为大部分的 rep 用的还是 vue1。

import VueRouter from 'vue-router'

Vue.use(VueRouter)

const NotFound = { template: '<div>not-found</div>' }
const routes = [
  { path: '/articles', component: HomePage },
  { path: '/articles/:number', component: ArticleDetail },
  { path: '', redirect: '/articles' },
  { path: '*', component: NotFound }
]

const router = new VueRouter({
  routes: routes
})

const app = new Vue({
  router: router,// 或简写成 router //(ES6)
  render: h => h(App)
}).$mount('#app')

routes 的顺序一定不能错了,路由是按顺序来匹配的,如果把 * 写在最前面,那么所有的路由将返回 NotFound 组件。这里是用来匹配 404 的,当然页面的样式可以随意设计,我这里只是为了演示。

<router-link> 应该是不能用相对路径的,这个有点不好用。<router-link> 的 to 属性不写或者 <a> 的 href 属性值为空或者 # 都是匹配到空( '' )路径,而不是像 ng1 的 ui-router 那样不跳转。而且 template 里是不能用 <a> 做跳转的,这个有点坑。

props

向子组件传递数据是经常会用到的,一般是在子组件定义 props 来接受数据,当父组件改变数据时子组件的数据也会进行更新。但这里是有一个坑的,先看代码:

<pagination :params="params"></pagination>

data: function() {
  return {
    params: {
      per_page: 3
    }
  }
},
created: function() {
  this.params.page = 1;
}

// child-component

<p> {{ params.per_page }} {{ params.page }} </p>// 3 1

props: {
   total: Number,
   params: Object
}

这里传递的是一个 object,在父组件中定义一个方法改变 params 的 per_page 和 page 的值,但子组件只有 params.per_page 的值更新了,page 的值是没有更新的。这是因为 vue 只会跟踪 data 函数里定义的变量,所以想要 page 也更新,在 data 的 params 里加上 page 属性就行了。

methods: {
  changeParams() {
    this.params.per_page = 5
    this.params.page = 2   
  }
}

// child-component

<p> {{ params.per_page }} {{ params.page }} </p>// 5 1

public-filter

vue 的文档只大概的介绍了怎么定义一个 filter,虽然也提到了在 vue 实例上定义公用 filter,但是并没有说明怎么在组件中使用定义好的公用 filter。这对像我一样刚接触 vue 的还是有点头疼。我探索了一会实现了,简直非常简单。首先,先定义,然后 import 就行了。

// fileName: formatDate.js

import Vue from 'vue'

Vue.filter('formatDate', function(value) {
  return new Date(value).toLocaleString()
})

const formatDate = Vue.filter('formatDate')

export default formatDate

// component

<p>Posted at {{article.created_at | formatDate}}</p> 

import formatDate from '../filters/formatDate.js'

当然不止是 filter,所有公用的 component、directive 都是一样的。

vue-cli

最后谈谈 vue-cli 脚手架的一个小问题,我下载的是基于 webpack 的项目配置。当我们运行 npm run build 编译打包我们的文件时,dist 目录的所有跟路径有关的如 css、js 的引入还有 css 里的图片都是绝对路径也就是根路径 / 。这个对于很多把 dist 目录下的文件放在网站的根目录下是没有任何问题的。但我是部署到 github-page (hugo-seth.github.io/blog/)上的,这个地址的根路径其实是 hugo-seth.github.io ,这个时候就会有问题了,所有的资源都是 404 ,网站是没办法用的,我是这样解决的,首先 打开 config 目录的 index.js 文件,把里面的build 的 assetsPublicPath 改为 ./

qq20170114-0

这样设置之后 dist 的 index.html 里的链接是没有问题了,但 css 里的图片引用还是有问题,所以我找了个 node 插件 replace-in-file,照文档写了个替换 css 里替换字符串路径的 js,如图,文件是 build/replace.js

qq20170114-1

但 template 里的图片路径有没问题我不太清楚,因为我没用到,不过就算有问题的话,在 replace.js 里加几行 js 就行了,这个就留给读者自己去探索了。

ejs 的 layout 实现方法

ejs本身是不支持的layout的,三年前就有人提了PR实现了layout和block(详情请戳),但一直都没被合并,不懂是因为什么。

在进去本文的主要内容之前,先回顾一下ejs的项目文档提供了哪几种用法(项目链接):

  • 表达式 <% code %>

  • 变量赋值,如果是html,标签将会转义 <%= code %>

  • 变量赋值,不转义 <%- code %>

  • 引入其他文件(html或ejs)<% include path %>

可以说功能已经比较齐全了。但是既然有 include,为什么没有 extend 呢?相信用过 jade 的朋友一定会发出这样一句怒吼。这就是我今天要讲的内容。

用 express 的同学不必担心这个问题,因为有一个插件叫ejs-locals,用法写得非常清楚,看一遍基本都会了。

那我要是用的 koa 呢?大家肯定会说:mdzz,肯定有插件的,搜一下不就行了。是的,插件是有,我搜了好久终于找到一个叫 koa-ejs 的(也许是我的英语太水了)。但不要高兴的太早,因为我估计没多少人能一下就看懂它的 README,反正我是看不懂。不过经过我的脑补及实践后已经彻底掌握了它的套路。

var koa = require('koa');
var render = require('koa-ejs');
var path = require('path');

var app = koa();
render(app, {
  root: path.join(__dirname, '/app/views'),
  layout: 'layout', // default layout
  viewExt: 'ejs',
  cache: false,
  debug: true
})

app.use(function *() {
  yield this.render('pages/index', {
    title: 'Hugo Blog',
    param: '<h2>Hello world</h2>',
    star: 'Mayday'
  })
});

其他的没什么好说的,主要是 layout 属性是什么意思呢。

qq20161207-0

qq20161207-1

qq20161207-2

index 和 layout 文件和 render 的html如上图,includes/header 里就一句 <h1>hello world</h1>。相信大家已经知道 layout 属性是什么意思了,就是将模板文件 extend 到哪个layout(默认就是 root属性设置的文件夹下的 layout.ejs)。这里就会有一个问题了,我们肯定不可能所有的页面都套同一个 layout 吧,那我们怎么修改它呢?如下:

app.use(function *() {
  yield this.render('pages/index', {
    layout: 'detail-layout', // 换成detail-layout.ejs or layout: false 不嵌套
    title: 'Hugo Blog',
    param: '<h2>Hello world</h2>',
    star: 'Mayday'
  })
});

知道了这些,我们就可以愉快的用 ejs 了。

谈谈怎样阅读 vue 的源码

最近发现自己太菜了,就在想怎么提高自己。从 github 上找新框架写几个 demo 或 todo 已经完全对自己没有帮助了,而我自己从来没有阅读过任何项目的源码。虽然解读源码的文章看过不少,但其实并没有多少帮助。既然很多前辈都说过读好项目的源码能提高水平,那我就亲自实践看看效果好了,我不想选简单没什么难度的项目,所以就从现在最火的三个前端框架之一的 vue 开始读。

这篇文章并不会具体的解读源码中具体的某个文件、某个函数或某行代码,因为我上面也说了,这样的源码解读对绝大多数人来说,并没有什么帮助。而我要说的是阅读 vue 的源码的方法,像 vue 现在已经非常庞大了,对于经验不够的前端同行来说,想要读懂源码难度都不会小。只有在我们掌握了正确的读源码姿势后,自己再去一行行的读源码才能真正的提高水平。

当然我只能谈谈我是怎样读 vue 源码的,并不能说它是正确的方式。我之前没有读过源码,从上周末开始,我已经花了一周多的时间学习 vue 的源码。我已经知道了 vue 运行的整个流程和一部分功能的实现细节。而对于一些非常复杂的功能,我只大概知道它在哪个步骤进行,具体如何实现的,还有待继续学习。

好了,言归正传。我说下我是怎样阅读 vue 源码的吧:

看文档

第一步是看文档,注意是看文档,而不是查文档。文档一定要看的是指南api,指南告诉我们了 vue 是什么,vue 的核心概念和一些核心功能的实现细节。api 文档则列出了所有的方法和属性,并一一做了介绍。

在用 vue 做项目的时候,我们都会在记不清某个 api 或不知道有没有某个 api 的时候去查文档。而在我们读源码的时候,如果不熟悉 api 的话会非常吃力的,因为你不知道 api 对应的源码方法能接受几个参数、什么类型的参数。特别是像 vue 的 api,作者为了方便使用者,很多 api 都接受几个类型的参数,自然会有很多判断的逻辑在源码里。所以不管你之前有没有看过文档,一定要先去认真的看一遍。不然你在读源码的过程中还是要时不时的打开文档的,就像我一样😓

画出项目的运行过程

看完文档后,我们就可以开始读源码了。在读源码的过程中我们要把运行过程画出来,并把每个过程做了什么一一列出,这样能帮助我们记忆,最重要的能方便查找,特别是属性的定义和赋值。很多编辑器能跳到函数的定义,但对属性和方法是无能为力的。我们看源码的过程都是边看边忘的,如果没有好的回顾方法,会很容易泄气的。我自己是用百度脑图来画流程图,它是在线的,而且支持搜索。先看一下我画的流程图:

vue

既然要画出整个流程图,那我们首先得知道项目的运行入口,再跟着运行过程一步步的记录。因为项目的 README 没写入口在哪,对于有经验的前端来说,就知道去查看 package.json 文件。我们先看 main 属性是 dist/vue.runtime.common.js,它并不是源码的入口。那我们只能看 scripts 里的运行指令了。dev 就是开发指令,它运行的是 scripts/config.js 文件并且 TARGET = web-full-dev,在 scripts/config.js 里能知道入口是 web/entry-runtime-with-compiler.js 文件。

注:npm 包的 package.json 的写法是有规范的,有兴趣的读者可自行去搜索。

找到入口后,我们就可以从入口开始,一行行的分析代码做了什么。可以发现我上面的流程图少了前面一部分,直接是从 src/core/index.js 文件开始的,因为 src/platform 文件里做的事非常复杂,但也非常简单,就是给 Vue 类的原型根据不同的平台扩展不同的 $mount 方法,具体逻辑可以在之后调用此方法的时候再回过头来看。

src/core/index.js 文件开始,我们可以将代码做的事简单总结如下:

  1. Vue 类添加属性和方法
  2. Vue 类的原型添加属性和方法
  3. 在调用 new Vue() 后给实例添加属性和方法

写示例 debugger 源码

虽然我们可以简单的画出流程图,但对于一些功能的实现,我们是需要反反复复看源码的。而且很多细节就算我们看了几遍源码,估计也不能理解的透彻。举例来说,在 new Vue() 之后,困扰我的第一个问题是第一次 options 的处理:

if (options && options._isComponent) {
   // 其实我们用 components 属性定义组件和用 Vue.component 方法定义组件
   // 都走得这个逻辑,如果没发现这个的话,就很难理解我们定义的
   // options 怎么变成 options 的 __proto__ 了
   // optimize internal component instantiation
   // since dynamic options merging is pretty slow, and none of the
   // internal component options needs special treatment.
   initInternalComponent(vm, options)
   } else {
   // 只有 new Vue 的时候会走这个逻辑
   // 合并 options 并处理 props、inject、directives 参数
   vm.$options = mergeOptions(
     resolveConstructorOptions(vm.constructor),
     options || {},
     vm
   )
}

虽然在上面的代码里,我已经注释了我的结论,但重要的是我们如何得出结论。首先我们可以全局搜索 _isComponent 关键字,这样我们可以知道它是在编译模板是加上的属性。但模板编译可以说是 vue 最复杂的逻辑之一了,我现在还看不太明白。那我就自己写一个有层级组件的 demo,看看每个组件的 options 是什么。通过 demo 我们就能知道不管是用 components 属性还是 Vue.component 方法定义组件,走的都是同一个逻辑,只有在 new Vue 的时候才是 mergeOptions

在此基础上,我们可以给源码的这个地方打上 debugger,然后 npm run dev 编译,刷新一下页面,我们就可以在 debugger 模式里验证我们的结论。

当我们想了解源码的细节时,debugger 通常是最有效的方式。

结语

阅读源码不会是件轻松的事,却是个提升技术的好方式。在读源码的过程中,我们不能浅尝辄止,一定要深入了解细节,反复阅读,相信肯定是有收获的,至少我是有收获的。

借助 workbox 将网站升级成 PWA

PWA(Progressive Web Apps)是谷歌近几年一直在推进的 web 应用新模型。PWA 借助 Service Worker 缓存网站的静态资源,甚至是网络请求,使网站在离线时也能访问。并且我们能够为网站指定一个图标添加在手机桌面,实现点击桌面图标即可访问网站。

Web App Manifest

Web App Manifest 是一个 JSON 文件,它用来定义网站添加到桌面的图标以及从桌面图标进入网站时的一系列行为,如:启动样式,全屏主题等。

先创建 manifest.json

{
  "name": "blog-pwa",
  "short_name": "blog-pwa",
  "icons": [
    {
      "src": "/img/icons/android-chrome-192x192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "/img/icons/android-chrome-512x512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ],
  "start_url": "/index.html",
  "display": "standalone",
  "background_color": "#000000",
  "theme_color": "#4DBA87"
}

将文件引入:

<link rel=manifest href=/manifest.json>

我们可以从开发者工具上看我们的配置:

1

icons 属性定义了添加到桌面的图标, display: standalone 表示我们要从桌面全屏启动,theme_color": "#4DBA87 是全屏启动时手机顶部状态栏的背景色,background_color": "#000000 是启动页的背景色,启动页目前不能定制,默认由 background_coloriconname 组合而成。

Web App Manifest很简单,只要照着文档每个属性看一遍就行。

Service Worker

Service Worker 是浏览器在后台独立于网页运行的脚本。是它让 PWA 拥有极快的访问速度和离线运行能力。

那它是如何做到的呢?我们一步步来看。

注册 Service Worker

if ('serviceWorker' in navigator) {
  navigator.serviceWorker
    .register('/service-worker.js')
    .then(registration => {
      console.log(
        'ServiceWorker registration successful with scope: ',
        registration.scope
      )
    })
    .catch(err => {
      console.log('ServiceWorker registration failed: ', err)
    })
}

需要注意的是,Service Worker 脚本除了域名为 localhost 时能运行在 http 协议下以外,只能运行 https 协议下。

安装

const CACHE_NAME = 'cache-v1'
const DATA_CACHE_NAME = 'data-cache-v1'

const PRE_CACHE = ['/index.html', '/css/app.css', '/js/app.js']

self.addEventListener('install', e => {
  console.log('[ServiceWorker] Install')
  e.waitUntil(
    caches.open(CACHE_NAME).then(cache => {
      return cache.addAll(PRE_CACHE)
    })
  )
})

在安装的时候预缓存网站的静态资源,任何资源路径出错都会造成 Service Worker 安装失败。

代理请求

self.addEventListener('fetch', e => {
  e.respondWith(
    caches.match(e.request).then(response => {
      if (response) {
        return response
      }

      const fetchRequest = e.request.clone()

      return fetch(fetchRequest).then(response => {
        // Check if we received a valid response
        if (!response || response.status !== 200) {
          return response
        }

        const responseToCache = response.clone()

        caches.open(DATA_CACHE_NAME).then(cache => {
          cache.put(e.request, responseToCache)
        })

        return response
      })
    })
  )
})

安装成功后,Service Worker 就可以监听网站的所有请求,匹配到缓存时直接返回,未匹配到时请求服务器,服务器成功返回时添加到缓存。

更新

现在网站的 Service Worker 已经可以正常工作了,那如何更新它呢?

我们只需要修改 Service Worker 文件就可以更新它。当我们每次访问网站时都会去下载这个文件,当发现文件不一致时,就会安装这个新 Service Worker ,安装成功后,它将进入等待阶段。当我们关闭窗口重新导航到网站时(刷新网页不行),新 Service Worker 将开始控制网站。旧 Service Worker 终止工作并触发 activate 事件:

self.addEventListener('activate', e => {
  e.waitUntil(
    caches.keys().then(keyList => {
      return Promise.all(
        keyList.map(key => {
          if (key !== CACHE_NAME && key !== DATA_CACHE_NAME) {
            console.log('[ServiceWorker] Removing old cache', key)
            return caches.delete(key)
          }
        })
      )
    })
  )
})

在其卸载时一定要删除旧缓存,不然我们的网站永远无法更新。

上面只简单讲了 Service Worker 如何工作。我们会发现有很多问题需要我们进一步解决:

  1. 预缓存的静态资源修改后在下一次发版本时的文件名都不一样,手动写死太低效,最好每次都自动生成资源文件名。
  2. 缓存资源是以硬编码字符串判断是否有效,这样每次发版本都需要手动修改,才能更新缓存。并且每次都是全量更新。能否以文件的粒度进行资源缓存呢?
  3. 请求代理没有区分静态资源和动态接口。已经缓存的动态接口也会一直返回缓存,无法请求新数据。

上面只列出了三个明显的问题,还有很多问题是没有考虑到的。如果让我们自己来解决这些问题,不仅是工作量很大,而且也很难写出生产环境可用的 Service Worker

workbox

既然如此,我们最好是站在巨人的肩膀上,这个巨人就是谷歌。workbox 是由谷歌浏览器团队发布,用来协助创建 PWA 应用的 JavaScript 库。当然直接用 workbox 还是太复杂了,谷歌还很贴心的发布了一个 webpack 插件,能够自动生成 Service Worker 和 静态资源列表 - workbox-webpack-plugin

只需简单一步就能生成生产环境可用的 Service Worker

const { GenerateSW } = require('workbox-webpack-plugin')

new GenerateSW()

打包一下:

2

还能说什么呢?谷歌大法好!当然这只是最简单的可用版本,其实这里有一个最严重的问题不知道有没人发现,那就是 importScripts 引用的是谷歌域名下的 cdn ,这让我们墙内的网站怎么用,所以我们需要把这个问题解决并自定义一些配置增强 Service Worker 的能力:

new GenerateSW({
  importWorkboxFrom: 'local',
  skipWaiting: true,
  clientsClaim: true,
  runtimeCaching: [
    {
      // To match cross-origin requests, use a RegExp that matches
      // the start of the origin:
      urlPattern: new RegExp('^https://api'),
      handler: 'staleWhileRevalidate',
      options: {
        // Configure which responses are considered cacheable.
        cacheableResponse: {
          statuses: [200]
        }
      }
    },
    {
      urlPattern: new RegExp('^https://cdn'),
      // Apply a network-first strategy.
      handler: 'networkFirst',
      options: {
        // Fall back to the cache after 2 seconds.
        networkTimeoutSeconds: 2,
        cacheableResponse: {
          statuses: [200]
        }
      }
    }
  ]
})

首先 importWorkboxFrom 我们指定从本地引入,这样插件就会将 workbox 所有源文件下载到本地,墙内开发者的福音。上面提到过新 Service Worker 安装成功后需要进入等待阶段,skipWaiting: true 将使其跳过等待,安装成功后立即接管网站,注意这个要和 clientsClaim 一起设置为 trueruntimeCaching 顾名思义是配置运行时如何缓存请求的,这里只说一点,缓存跨域请求时 urlPattern 的值必须为 ^ 开头的正则表达式,其它的配置看文档都能得到详细的介绍。

再打包一次:

3

现在我们就可以将打包好的代码部署到网站上了,源码在这,最后再上几张图:

4

动图

参考

Web App Manifest

服务工作线程:简介

服务工作线程生命周期

workbox-webpack-plugin

MVVM 中的动态数据绑定

上一篇文章我们了解了怎样实现一个简单模板引擎。但这个模板引擎只适合静态模板,因为它是将模板整体编译成字符串进行全量替换。如果每次数据改变都进行一次替换,会有两个最主要的问题:

  1. 性能差。DOM 操作本身就非常大的开销,更别说每一次都替换这么大的量。
  2. 破坏事件绑定。这个是最麻烦的,如果我们没有给解绑移除 DOM 绑定的事件,还会造成内存泄露。而且每一次替换都要重新绑定事件。

因此,没有人会将这种模板引擎用来编译动态模板。那我们如何编译动态模板呢?

回答这个问题之前,我们先要了解前端的世界何时出现了动态模板:它是由 MVVM 框架带来的,动态模板是 MVVM 框架的视图层(view)。我们知道的 MVVM 框架有 knockout.jsangular.jsavalonvue

对于这些框架,大部分人最熟悉的应该就是 vue,所以我下面也是以 vue 1.0 作为参考,来实现一个功能更简单的动态模板引擎。它是框架自带的一个功能,让框架能够响应数据的改变。从而刷新页面。

MVVM 动态模板的特点是能最小化刷新:哪个变量改变了,与之相关的节点才会更新。这样我们就能避免上面提到的静态模板的两大问题。

要实现最小化刷新,我们要将模板中的每个绑定都收集起来。这个收集工作是框架在完成第一次渲染前就已经完成了,每个绑定都会生成一个 Directive 实例:

class Directive {
  constructor(vm, el, exp, update) {
    this.vm = vm
    this.el = el
    this.exp = exp
    this.update = update
    this.watchers = []
    this.get = getEvaluationFn(exp).bind(this, vm.$data)

    this.bind()
  }
}

function getEvaluationFn(exp) {
  return new Function('data', 'with(data) { return ' + exp + '}')
}

我们知道,每个绑定都由指令和指令值(指令值可能是表达式,可能是语句,也可能就是一个变量,还可能是框架自定义的语法)构成,每种指令都有对应的刷新函数(update)。如节点值的绑定的刷新函数是:

function updateTextNode() {
  const value = this.get()
  this.el.nodeValue = value
  console.log(this.exp + ' updated: ' + value)
}

有了刷新函数,那如何做到在数据改变时调用刷新函数更新节点的值呢?我们就还要将每个指令里的相关变量都跟这个 Directive 实例关联起来。我们用一个 $binding 对象来记录,它的键是变量,值是 Binding 实例:

class Binding {
  constructor() {
    this.subs = []
  }

  addChild(key) {
    return this[key] || new Binding()
  }

  addSub(watcher) {
    this.subs.push(watcher)
  }
}

那上面的 subs 里添加的为什么不是 Directive 实例呢,而是 watcher 呢?它其实是 Watcher 的实例,这是为了以后能够实现 $watch 方法提前引入的概念,Watcher 实例的 cb 既可以是指令的刷新函数,也可以是 $watch 方法的回调函数:

class Watcher {
  constructor(vm, path, cb, ctx) {
    this.id = ++uid
    this.vm = vm
    this.path = path
    this.cb = cb
    this.ctx = ctx || vm

    this.addDep()
  }
}
class Directive {
  bind() {
    this.watchers.push(new Watcher(this.vm, this.exp, this.update, this))
  }
}

我们先考虑最简单的情况,指令值就是一个变量,根据上面的思路,我们就可以写出最简单的实现了,代码就不贴了,有兴趣的直接看源码

<div id="app">
  <h1>MVVM</h1>
  <p>
    <span>My name is {{name.first}}-{{name.last }},</span>{{age}} years old
  </p>
</div>
<script src="../dist/eve.js"></script>
<script>
    const app = new Eve({
      el: '#app',
      data: {
        name: {
          first: 'hugo',
          last: 'seth'
        },
        age: 1
      }
    })
    console.log(app)
</script>

untitled

上面实现的动态模板是在我们假定了指令值是最简单的变量的情况下实现的。那要是把上面的模板改为下面这样呢?

<h1>MVVM</h1>
  <p>
      <span>My name is {{name.first}}-{{name.last }},</span>{{'age: ' + age}} years old
  </p>
  <p>salary: {{ salary.toLocaleString() }}</p>

那我们上面的实现有一些数据就不能动态刷新了,原因很简单,就是我们是直接将 'age: ' + ageDirective 实例关联,而我们修改的只是 age,自然就找不到对应的实例了。那我们如何解决呢?

首先想到的肯定是按照现有的实现来扩展,让它支持模板插值是表达式的情况。已有的实现是直接解析得到变量,那我们就继续想办法直接解析表达式得到变量。像 'age: ' + age 这种表达式直接解析出 age 其实不难。但 salary.toLocaleString() 这种就不好做了,要是 salary.toLocaleString().slice(1) 这种可以说是没办法解析了。

既然这条路行不通,其实我们是有更简单的方法。既然我们都已经将 data 进行了代理,那我们就可以在 get 获取变量值时进行依赖收集。因为我们本来就会运行 Directive 实例的求值函数进行初始值的替换,这就会触发变量的 get 。具体的代码怎么写就不说了,详细的修改支持表达式的源码

2

当然现在只实现动态模板最简单的插值指令。还有一些更复杂的指令如:iffor 的实现方式,读者可以前往参考链接学习。

参考

vue早期源码学习系列之四:如何实现动态数据绑定

由二分搜索算法引发的代码优化思考

代码地址:https://github.com/Hugo-seth/exercises/blob/master/binary-search/index.js

最近在做 JS 的代码练习,昨天要实现的是“二分搜索”算法,点击这里查看要求

这个算法其实不难,我们只要知道数组的 sort 方法是如何比较大小的就能很快的实现:

function binarySearch(arr, item) {
  let result;

  (function _search(_arr) {
    if (_arr.length > 0) {
      let midIndex = Math.floor(_arr.length / 2)

      let _result = _compare(_arr[midIndex], item)
      if (_result === 0) {
        result = arr.indexOf(item)
      } else if (_result > 0) {
        _search(_arr.slice(0, midIndex))
      } else {
        _search(_arr.slice(midIndex + 1))
      }
    } else {
      result = -1
    }
  })(arr)

  return result
}

function _compare(a, b) {
  let _a = String(a),
    _b = String(b),
    _a_len = _a.length,
    _b_len = _b.length,
    length = Math.min(_a_len, _b_len)

  for (let i = 0; i < length; i++) {
    let _a_code = _a.charCodeAt(i),
      _b_code = _b.charCodeAt(i)

    if (_a_code > _b_code) {
      return 1
    } else if (_a_code < _b_code) {
      return -1
    }
  }

  if (_a_len > _b_len) {
    return 1
  } else if (_a_len < _b_len) {
    return -1
  } else {
    return 0
  }
}

module.exports = binarySearch

上面的难点是 _compare 函数的实现,先将比较的两数转换成 String,再比较每一位的 charCode

我们再仔细看 _search 函数的代码,如果找到的话:result = arr.indexOf(item),我们想一下如果可以用 indexOf 方法的话哪还需要自己写什么“二分搜索”,这不就是个伪命题╮(╯▽╰)╭

所以我们改一下 binarySearch 函数:

function binarySearch(arr, item) {
  let result, startIndex = 0;

  (function _search(arr, item) {
    if (arr.length > 0) {
      let midIndex = Math.floor(arr.length / 2)

      let _result = _compare(arr[midIndex], item)
      if (_result === 0) {
        result = startIndex + midIndex
      } else if (_result > 0) {
        _search(arr.slice(0, midIndex), item)
      } else {
        startIndex += (midIndex + 1)
        _search(arr.slice(midIndex + 1), item)
      }
    } else {
      result = -1
    }
  })(arr, item)

  return result
}

我们新增了一个变量来保存每次查找时的开始序号,在递归时更新它,最后根据它得出结果。这下看起来好像没问题了,我们运行一下测试命令:npm test

还有一条没通过,最后一项测试竟然触发了 5000 次数组项的查找!

qq20170426-160659

刚看到这个,我是没懂为什么会触发了这么多次的查找。仔细 debugger 了之后,发现是数组的 slice 方法会触发数组项的 get 。但这好像是无法解决的问题。所以我提交的时候还写了如下注释:

qq20170426-161345

事实证明还是太年轻,晚上睡觉的时候突然想到为什么要传数组进去,内存开销也挺大的,直接传开始序号和结束序号进去不就行了,这样就只会在调用 _compare 方法的时候才触发一次 get 。修改代码如下:

function binarySearch(arr, item) {
  let result,
    startIndex = 0,
    endIndex = arr.length - 1;

  (function _search(startIndex, endIndex) {
    if (endIndex - startIndex >= 0) {
      let midIndex = Math.floor((startIndex + endIndex) / 2)

      let _result = _compare(arr[midIndex], item)
      if (_result === 0) {
        result = midIndex
      } else if (_result > 0) {
        endIndex = midIndex - 1
        _search(startIndex, endIndex)
      } else {
        startIndex = midIndex + 1
        _search(startIndex, endIndex)
      }
    } else {
      result = -1
    }
  })(startIndex, endIndex)

  return result
}

终于,我们完成的算法可以通过所有的测试了。

总结

第一个版本的代码考虑不足,用了不能用的方法。第二个版本只是最后一项测试通不过,而实际使用的话是不会有问题的。但却有值得优化的地方:

  1. 每次都传入一个新数组,内存的开销不小

  2. 每次通过 slice 方法生成新数组的时候都要访问新数组的项。虽然浏览器会替我们优化:只在第一次 slice 的时候访问了新数组的所有项,后续的 slice 应该是用的缓存所以没有访问。

第三个版本解决了上面两个问题:不再传入数组,而是每次传入两个 Number ,并且不再需要使用 slice 方法。

实现一个简单的模板引擎

对现在的前端来说,模板是非常熟悉的概念。毕竟现在三大框架那么火,不会用框架还能叫前端吗🐶,而框架是必定有模板的。那我们写的模板是如何转换成 HTML 显示在网页上的呢?

我们先从简单的说起,静态模板一般用于需要 SEO 且页面数据是动态的网页。由前端编写好静态模板,后端负责将动态的数据和静态模板交给模板引擎,最终编译成 HTML 字符串返回给浏览器。这种时候我们用到的模板引擎可能是远古的 jsp,或是现在用的比较多的 pug(原来叫 jade)、ejs。

模板引擎做的就是编译模板的工作。它说白了就是一个函数:将模板字符串转换成 HTML 字符串。

我们先写一个最简单的静态模板编译函数:

正则替换

我们的模板和数据如下:

const tpl = '<p>hello,我是{{name}},职业:{{job}}<p>'

const data = {
  name: 'hugo',
  job: 'FE'
}

那我们想到的最简单的办法就是正则替换,当然我们别忘了要把前缀加上,name 要转换成 data.name

function compile(tpl, data) {
  const regex = /\{\{([^}]*)\}\}/g
  const string = tpl.trim().replace(regex, function(match, $1) {
    if ($1) {
      return data[$1]
    } else {
      return ''
    }
  })
  console.log(string) // <p>hello,我是hugo,职业:FE<p>
}

compile(tpl, data)

上面的编译函数在例子中是可以工作的,但要是我把模板和数据改一下呢?

const tpl = '<p>hello,我是{{name}},年龄:{{info.age}}<p>'

const data = {
  name: 'hugo',
  info: {
    age: 26
  }
}

这个时候控制台打印的就是:

<p>hello,我是hugo,年龄:undefined<p>

因为 data["info.age"] 的值是 undefined 。所以我们还要处理正则匹配到的字符串,这个时候再用正则已经非常不好做了。既然这样,不如就直接全改用字符串匹配:

字符串解析

function compile(tpl) {
  let string = ''
  tpl = tpl.trim()
  while (tpl) {
    const start = tpl.indexOf('{{')
    const end = tpl.indexOf('}}')
    if (start > -1 && end > -1) {
      if (start > 0) {
        string += JSON.stringify(tpl.slice(0, start))
      }
      string += '+ data.' + tpl.slice(start + 2, end).trim() + ' +'
      tpl = tpl.slice(end + 2)
    } else {
      string += JSON.stringify(tpl)
      tpl = ''
    }
  }
  console.log(string)
  // "<p>hello,我是"+ data.name +",年龄:"+ data.info.age +"<p>"

  return new Function('data', 'return ' + string)
}

compile(tpl)(data) // <p>hello,我是hugo,年龄:26<p>

这样我们新的编译函数就可以处理 {{info.age}} 这种嵌套属性的情况了。上面的 JSON.stringify 作用是给字符串的两端加上 ",然后转义字符串中的特殊字符。

虽然我们解决了嵌套属性的问题,但又面临更困难的问题,就是怎样让模板里插值支持像 {{ '名字是: ' + name }} 这样表达式。在这种情况下,我们是很难在每个正确的地方加 data. 前缀的,因为前缀只能加上变量前,而表达式里可能还有字符串。

使用 with 语句

我们考虑最简单的处理方式,也就是不加前缀了,使用 with 语句指定变量的作用域。所以我们只要编译后返回一个函数,在这个函数内使用 with 语句指定作用域,函数再返回 HTML 字符串。在下面的例子中,我使用的是 ejs 模板的语法:

const tpl = `<p>hello,我的<%= '名字是: ' + name %>,年龄:<%= info.age %><p>`

const data = {
  name: 'hugo',
  info: {
    age: 26
  }
}
function compile(tpl) {
  const ret = []
  tpl = tpl.trim()
  ret.push('var _data_ = [];')
  ret.push('with(data) {')
  while (tpl) {
    let start = tpl.indexOf('<%=')
    const end = tpl.indexOf('%>')
    if (start > -1 && end > -1) {
      if (start > 0) {
        ret.push('_data_.push(' + JSON.stringify(tpl.slice(0, start)) + ');')
      }
      ret.push('_data_.push(' + tpl.slice(start + 3, end) + ');')
      tpl = tpl.slice(end + 2)
    } else {
      ret.push('_data_.push(' + JSON.stringify(tpl) + ');')
      tpl = ''
    }
  }
  ret.push('}')
  ret.push('return _data_.join("")')
  return new Function('data', ret.join('\n'))
}

const fn = compile(tpl)
fn(data)
// <p>hello,我的名字是: hugo,年龄:26<p>

上面的编译函数将模板根据模板语法 <%=%> 分割成各个部分放入数组中,再将数组中的元素由换行符连接,成为 new Function 的函数体,生成的函数如下:

function(data/*``*/) {
  var _data_ = [];
  with(data) {
    _data_.push("<p>hello,我的");
    _data_.push('名字是: ' + name);
    _data_.push(",年龄:");
    _data_.push(info.age);
    _data_.push("<p>");
  }
  return _data_.join("")
}

我们再将 data 作为参数传入这个函数就可以得到期望的 HTML 字符串。

现在我们已经实现了能够编译插值是表达式的模板引擎。但我们还差一个非常重要的功能,那就是编译模板中的语句,如:for 循环和 if 语句。要实现编译语句的功能,我们必须将语句和插值区分开,因此要使用不同的模板语法:语句用 <% %>,插值则用<%= %>。那我们就可以将上面的编译函数稍微修改下,根据不同的语法分别处理,就可以支持模板语句了:

const tpl = `
<p>hello,我是<%= name + '-seth' %>,年龄:<%= info.age %><p>
<% if (info.age > 18 && info.age < 28){ %>
  <p>是个九零后中年人</p>
<% } %>
<h3>兴趣</h3>
<ul>
  <% for (var i = 0; i < interests.length; i++) { %>
    <li><%= interests[i] %></li>
  <% } %>
</ul>
`
const data = {
  name: 'hugo',
  info: {
    age: 26
  },
  interests: ['movie']
}
function compile(tpl) {
  const ret = []
  tpl = tpl.trim()
  ret.push('var _data_ = [];')
  ret.push('with(data) {')
  while (tpl) {
    let start = tpl.indexOf('<%')
    const end = tpl.indexOf('%>')
    if (start > -1 && end > -1) {
      if (start > 0) {
        ret.push('_data_.push(' + JSON.stringify(tpl.slice(0, start)) + ');')
      }
      if (tpl.charAt(start + 2) === '=') {
        ret.push('_data_.push(' + tpl.slice(start + 3, end) + ');')
      } else {
        ret.push(tpl.slice(start + 2, end))
      }
      tpl = tpl.slice(end + 2)
    } else {
      ret.push('_data_.push(' + JSON.stringify(tpl) + ');')
      tpl = ''
    }
  }
  ret.push('}')
  ret.push('return _data_.join("")')
  return new Function('data', ret.join('\n'))
}
const fn = compile(tpl)
fn(data)
// <p>hello,我的名字是: hugo,年龄:26<p>

//   <p>是个九零后中年人</p>

// <h3>兴趣</h3>
// <ul>
  
//     <li>movie</li>
  
// </ul>

这个修改后的编译函数没什么好解释的,就是根据不同的模板语法做不同的处理,最终返回的函数如下:

function(data /*``*/ ) {
  var _data_ = [];
  with(data) {
    _data_.push("<p>hello,我的");
    _data_.push('名字是: ' + name);
    _data_.push(",年龄:");
    _data_.push(info.age);
    _data_.push("<p>\n");
    if (info.age > 18 && info.age < 28) {
      _data_.push("\n  <p>是个九零后中年人</p>\n");
    }
    _data_.push("\n<h3>兴趣</h3>\n<ul>\n  ");
    for (var i = 0; i < interests.length; i++) {
      _data_.push("\n    <li>");
      _data_.push(interests[i]);
      _data_.push("</li>\n  ");
    }
    _data_.push("\n</ul>");
  }
  return _data_.join("")
}

这样我们就已经完成了一个功能简单的模板引擎。

Angular 学习笔记之 ui-router

Angular有官方的路由功能,也就是ng-router。但我们在实际的开发过程中都不会用到,而是使用来自第三方的路由:ui-router。原因非常简单,那就是ui-router的功能可以把ng-router秒成渣。而它的最强之处无疑就是支持视图(ui-view)及状态(state)的嵌套。由于我还没有需要用到ui-view嵌套的情况,正确来说因为视图比较简单,我们是用ng-include避免了ui-view嵌套的发生,在我看来ui-view嵌套写法会更难理解,如果想了解,可以参考这篇文章。所以我只会谈到state嵌套。

因为是第三方的插件,首先我们得在定义module注入ui-router,这样我们才能使用。

angular .module('test', [ 'ui.router']);

qq 20160703180342

我们首先需要配置router,注入$stateProvider和$urlRouterProvider,时刻记住:要想使用一个功能,必须先注入。其实这里还配置了两个非常有用的全局变量$state和$stateParams,稍后会详述。

上图可以看到state的嵌套写法:abstract和absolute是一个意思,写哪个都行,表示这个state是父state,以父state.开头的都是子state,例如client.home就是client的子state。 上图的例子中子state的url有两种情况(父state的url写不写):写的话,那子state的url都会继承这个url,例如menmber.add的url就会是:/member/add,没写的话就直接是子state定义的url。那我们子state出现在父state的什么地方呢?上图中父state的template的div里有个ui-view属性,所以,子state的template会append在父state的template的有ui-view属性的标签里<div ui-view></div>。这样我们就可以把公用的视图放在父template里,例如侧边导航。

我们配置好了state和url,我们在本地起服务器运行我们的angular页面就可以切换url了。接下来谈谈上文提到的非常有用的$state和$stateParams。

qq20160704-0

我们在url写了?就说明我们配置了$stateParams对象,例如

/shops?status&provinceCode&cityCode&districtCode&name&page

首先我们先要理解这个url是什么意思,/表示这是一个新的路径,?表示这是同一个路径下的参数。

qq20160704-1

我们看一下这个url下的$stateParams对象:

qq20160704-2

可以非常明显的看出,url上的参数和$stateParams对象的属性是一一对应的,没有赋值的参数就是undefined。这时候我们就可以在controller里注入$stateParams实现向后台查询的等功能。

接下来我们来看一下$state对象:

qq20160704-3

这里有一些非常有用的属性,我就不一一解释了,大家只要把它console.log出来看一下就明白了。我只会讲我们常用的属性,第一个是它的go方法,它可以实现我们跳转到任何存在的url上,还可以加上参数,例如:

$state.go('admin.shopsFilter.shops', { provinceCode: 12, cityCode: 1202, districtCode: 120204, page: 1 });

第一个参数就是state,跳转到的是对应的url,这一点要清楚。第二个就是参数就是url参数。

qq20160704-4

第二个是它的params属性,从上图我们可以看出其实和$stateParams是一样的。大家肯定觉得这很正常,但不寻常的是我是在admin.shopsFilter也就是父state的controller里log出来的,在这个controller里$stateParams是空对象,因为父state的url是没有参数的,但是$state的params属性是可以得到子state的参数的,这是非常有用的一个属性,大家在实际的开发中肯定会感受到的。

再给大家介绍一个跟state有关的有用的方法:

qq20160704-5

当切换到ui-sref的state对应的url时,就会自动给元素加上active的class(当然className是可以自定义的,不一定要active),这样我们不用任何多余的js就可以实现一些样式了。

【译】Redux 还是 Mobx,让我来解决你的困惑!

原文地址:Redux or MobX: An attempt to dissolve the Confusion
原文作者:rwieruch

我在去年大量的使用了 Redux,但我最近都在使用 Mobx 来做状态(state)管理。似乎现在社区里关于该选什么来替代 Redux 很自然地成为了一件困惑的事。开发者不确定该选择哪种解决方案。这个问题并不只是出现在 Redux 与 Mobx 上。无论何时,只要存在选择,人们就会好奇最好的解决问题的方式是什么。我现在写的这些是为了解决 Redux 和 Mobx 这两个状态管理库之间的困惑。

大部分的文章都用 React 来介绍 Mobx 和 Redux 的用法。但是在大部分情况下你都可以将 React 替换成 Angular 、 Vue 或其他。

在 2016 年年初的时候我用 React + Redux 写了一个相当大的应用。在我发现可以使用 Mobx 替代 Redux 时,我花时间将应用从 Redux 重构成了 Mobx 。现在我可以非常自在的使用它俩并且解释它俩的用法。

这篇文章将要讲什么呢?如果你不打算看这么长的文章(TLDR:too long, didn't read(查看此链接请自备梯子)),你可以看下目录。但我想给你更多细节:第一,我想简单地回顾状态管理库为我们解决了什么问题。毕竟我们写 React 时只用 setState() 或写其他 SPA 框架时用 setState() 类似的方法一样也可以做的不错。第二,我会大致的说下它们之间的相同之处和不同之处。第三,我会给 React 生态初学者指明怎样学习 React 的状态管理。友情提醒:在你深入 Mobx 和 Redux 之前,请先使用 setState() 。最后,如果你已经有一个使用了 Mobx 或 Redux 的应用,我将会就如何从其中一个状态管理库重构到另一个给你更多我的理解。


目录

  • 我们要解决的是什么问题?
  • Mobx 和 Redux 的不同?
  • React 状态管理的学习曲线
  • 尝试另一个状态管理方案?
  • 最后思考

我们要解决的是什么问题?

所有人都想在应用中使用状态管理。但它为我们解决了什么问题?很多人开始一个小应用时就已经引入一个状态管理库。所有人都在谈论 Mobx 和 Redux ,不是吗?但大部分应用在一开始的时候并不需要大型的状态管理。这甚至是危险的,因为这部分人将无法体验 Mobx 和 Redux 这些库所要解决的问题。

如今的现状是要用组件(components)来构建一个前端应用。组件有自己的内部状态。举个栗子,在 React 中上述的本地状态是用this.statesetState()来处理。但本地状态的状态管理在膨胀的应用中很快会变得混乱,因为:

  • 一个组件需要和另一个组件共享状态
  • 一个组件需要改变另一个组件的状态

到一定程度时,推算应用的状态将会变得越来越困难。它就会变成一个有很多状态对象并且在组件层级上互相修改状态的混乱应用。在大部分情况下,状态对象和状态的修改并没有必要绑定在一些组件上。当你把状态提升时,它们可以通过组件树得到

所以,解决方案是引入状态管理库,比如:Mobx 或 Redux。它提供工具在某个地方保存状态、修改状态和更新状态。你可以从一个地方获得状态,一个地方修改它,一个地方得到它的更新。它遵循单一数据源的原则。这让我们更容易推断状态的值和状态的修改,因为它们与我们的组件是解耦的。

像 Redux 和 Mobx 这类状态管理库一般都有附带的工具,例如在 React 中使用的有 react-reduxmobx-react,它们使你的组件能够获得状态。一般情况下,这些组件被叫做容器组件(container components),或者说的更加确切的话,就是连接组件( connected components )。只要你将组件升级成连接组件,你就可以在组件层级的任何地方得到和更改状态。

Mobx 和 Redux 的不同?

在我们深入了解 Redux 和 Mobx 的不同之前,我想先谈谈它们之间的相同之处。

这两个库都是用来管理 JavaScript 应用的状态。它们并不一定要跟 React 绑定在一起,它们也可以在 AngularJs 和 VueJs 这些其他库里使用。但它们与 React 的理念结合得非常好。

如果你选择了其中一个状态管理方案,你不会感到被它锁定了。因为你可以在任何时候切换到另一个解决方案。你可以从 Mobx 换成 Redux 或从 Redux 换成 Mobx。我下面会展示如何能够做到。

Dan Abramov 的 Redux 是从 flux 架构派生出来的。和 flux 不同的是,Redux 用单一 store 而不是多个 store 来保存 state,另外,它用纯函数替代 dispatcher 来修改 state,如果你对 flux 不熟并且没接触过状态管理,不要被这段内容所烦恼。

Redux 被 FP(函数式编程)原则所影响。FP 可以在 JavaScript 中使用,但很多人有面向对象语言的背景,比如 Java。他们在刚开始的时候很难适应函数式编程的原则。这就是为什么对于初学者来说 Mobx 可能更加简单。

既然 Redux 拥抱 FP,那它使用的就是纯函数。一个接受输入并返回输出并且没有其他依赖的纯函数。一个纯函数在相同的输入下输出总是相同而且没有任何副作用。

(state, action) => newState

你的 Redux state 是不可变的,你应该总是返回一个新的 state 而不是修改原 state。你不应该执行 state 的修改或依据对象引用的更改。

// don't do this in Redux, because it mutates the array
function addAuthor(state, action) {
  return state.authors.push(action.author);
}

// stay immutable and always return a new object
function addAuthor(state, action) {
  return [ ...state.authors, action.author ];
}

最后,在 Redux 的习惯用法里,state 的格式是像数据库一样标准化的。实体之间只靠 id 互相引用,这是最佳实践。虽然不是每个人都这样做,你也可以使用 normalizr 来使 state 标准化。标准化的 state 让你能够保持一个扁平的 state 和保持实体为单一数据源。

{
  post: {
    id: 'a',
    authorId: 'b',
    ...
  },
  author: {
    id: 'b',
    postIds: ['a', ...],
    ...
  }
}

Michel Weststrate 的 Mobx 则是受到面向对象编程和响应式编程的影响。它将 state 包装成可观察的对象,因此你的 state 就有了 Observable 的所有能力。state 数据可以只有普通的 setter 和 getter,但 observable 让我们能在数据改变的时候得到更新的值。

Mobx 的 state 是可变的,所以你直接的修改 state :

function addAuthor(author) {
  this.authors.push(author);
}

除此之外,state 实体保持嵌套的数据结构来互相关联。你不必标准化 state,而是让它们保持嵌套。

{
  post: {
    id: 'a',
    ...
    author: {
      id: 'b',
      ...
    }
  }
}

单 store 与多 stores

在 Redux 中,你将所有的 state 都放在一个全局的 store。这个 store 对象就是你的单一数据源。另一方面,多个 reducers 允许你修改不可变的 state。

Mobx 则相反,它使用多 stores。和 Redux 的 reducers 类似,你可以在技术层面或领域进行分治。也许你想在不同的 stores 里保存你的领域实体,但仍然保持对视图中 state 的控制。毕竟你配置 state 是为了让应用看起来更合理。

从技术层面来说,你一样可以在 Redux 中使用多个 stores。没有人强迫你只能只用一个 store。 但那不是 Redux 建议的用法。因为那违反了最佳实践。在 Redux 中,你的单 store 通过 reducers 的全局事件来响应更新。

如何使用?

你需要跟随下面的代码学习使用 Redux,首先在全局 state 上新增一个 user 数组。你可以看到我通过对象扩展运算符来返回一个新对象。你同样可以在 ES6(原文为 ES5,实际是应该是 ES6)中使用 Object.assign() 来操作不可变对象。

const initialState = {
  users: [
    {
      name: 'Dan'
    },
    {
      name: 'Michel'
    }
  ]
};

// reducer
function users(state = initialState, action) {
  switch (action.type) {
  case 'USER_ADD':
    return { ...state, users: [ ...state.users, action.user ] };
  default:
    return state;
  }
}

// action
{ type: 'USER_ADD', user: user };

你必须使用 dispatch({ type: 'USER_ADD', user: user });来为全局 state 添加一个新 user 。

在 Mobx 中,一个 store 只管理一个子 state(就像 Redux 中管理子 state 的 reducer),但你可以直接修改 state 。@observable 让我们可以观察到 state 的变化。

class UserStore {
  @observable users = [
    {
      name: 'Dan'
    },
    {
      name: 'Michel'
    }
  ];
}

现在我们就可以调用 store 实例的方法:userStore.users.push(user);。这是一种最佳实践,虽然使用 actions 去操作 state 的修改更加清楚明确。

class UserStore {
  @observable users = [
    {
      name: 'Dan'
    },
    {
      name: 'Michel'
    }
  ];

  @action addUser = (user) => {
    this.users.push(user);
  }
}

在 Mobx 中你可以加上 useStrict() 来强制使用 action。现在你可以调用 store 实例上的方法:userStore.addUser(user); 来修改你的 state 。

你已经看到如何在 Redux 和 Mobx 中更新 state 。它们是不同的,Redux 中 state 是只读的,你只能使用明确的 actions 来修改 state ,Mobx 则相反,state 是可读和写的,你可以不使用 actions 直接修改 state,但你可以 useStrict() 来使用明确的 actions 。

React 状态管理的学习曲线

React 应用广泛使用 Redux 和 Mobx 。但它们是独立的状态管理库,可以运用在除 React 的任何地方。它们的互操作库让我们能简单的连接React 组件。Redux + React 的 react-redux 和 MobX + React 的 mobx-react 。稍后我会说明它俩如何在 React 组件树中使用。

在最近的讨论中,人们在争论 Redux 的学习曲线。这通常发生在下面的情境中:想使用 Redux 做状态管理的 React 初学者。大部分人认为 React 和 Redux 本身都有颇高的学习曲线,两者结合的话会失控。一个替代的选择就是 Mobx ,因为它更适合初学者。

然而,我会建议 React 的初学者一个学习状态管理的新方法。先学习
React 组件内部的状态管理功能。在 React 应用,你首先会学到生命周期方法,而且你会用 setState()this.state 解决本地的状态管理。我非常推荐上面的学习路径。不然你会在 React 的生态中迷失。在这条学习路径的最后,你会认识到组件内部管理状态难度越来越大。毕竟那是 The Road to learn React 书里如何教授 React 状态管理的方法。

现在我们重点讨论 Redux 和 Mobx 为我们解决了什么问题?它俩都提供了在组件外部管理应用状态的方法。state 与组件相互解耦,组件可以读取 state ,修改 state ,有新 state 时更新。这个 state 是单一数据源。

现在你需要选择其中一个状态管理库。这肯定是要第一时间解决的问题。此外,在开发过相当大的应用之后,你应该能很自如使用 React 。

初学者用 Redux 还是 Mobx ?

一旦你对 React 组件和它内部的状态管理熟悉了,你就能选择出一个状态管理库来解决你的问题。在我两个库都用过后,我想说 Mobx 更适合初学者。我们刚才已经看到 Mobx 只要更少的代码,甚至它可以用一些我们现在还不知道的魔法注解。

用 Mobx 你不需要熟悉函数式编程。像“不可变”之类的术语对你可能依然陌生。函数式编程是不断上升的范式,但对于大部分 JavaScript 开发者来说是新奇的。虽然它有清晰的趋势,但并非所有人都有函数式编程的背景,有面向对象背景的开发者可能会更加容易适应 Mobx 的原则。

注:Mobx 可以很好的在 React 内部组件状态管理中代替 setState,我还是建议继续使用 setState() 管理内部状态。但链接文章很清楚的说明了在 React 中用 Mobx 完成内部状态管理是很容易的。

规模持续增长的应用

在 Mobx 中你改变注解过的对象,组件就会更新。Mobx 比 Redux 使用了更多的内部魔法实现,因此在刚开始的时候只要更少的代码。有 Angular 背景的会觉得跟双向绑定很像。你在一个地方保存 state ,通过注解观察 state ,一旦 state 修改组件会自动的更新。

Mobx 允许直接在组件树上直接修改 state 。

// component
<button onClick={() => store.users.push(user)} />

更好的方式是用 store 的 @action

// component
<button onClick={() => store.addUser(user)} />

// store
@action addUser = (user) => {
  this.users.push(user);
}

用 actions 修改 state 更加明确。上面也提到过,有个小功能可以强制的使用 actions 修改 state 。

// root file
import { useStrict } from 'mobx';

useStrict(true);

这样的话第一个例子中直接修改 store 中的 state 就不再起作用了。前面的例子展示了怎样拥抱 Mobx 的最佳实践。此外,一旦你只用 actions ,你就已经使用了 Redux 的约束。

在快速启动一个项目时,我会推荐使用 Mobx ,一旦应用开始变得越来越大,越来越多的人开发时,遵循最佳实践就很有意义,如使用明确的 actions 。这是拥抱 Redux 的约束:你永远不能直接修改 state ,只能使用 actions 。

迁移到 Redux

一旦应用开始变得越来越大,越来越多的人开发时,你应该考虑使用 Redux 。它本身强制使用明确的 actions 修改 state 。action 有 type 和 payload 参数,reducer 可以用来修改 state 。这样的话,一个团队里的开发人员可以很简单的推断 state 的修改。

// reducer
(state, action) => newState

Redux 提供状态管理的整个架构,并有清晰的约束规则。这是 Redux 的成功故事

另一个 Redux 的优势是在服务端使用。因为我们使用的是纯 JavaScript ,它可以在网络上传输 state 。序列化和反序列化一个 state 对象是直接可用的。当然 Mobx 也是一样可以的。

Mobx 是无主张的,但你可以通过 useStrict() 像 Redux 一样使用清晰的约束规则。这就是我为什么没说你不能在扩张的应用中使用 Mobx ,但 Redux 是有明确的使用方式的。而 Mobx 甚至在文档中说:“ Mobx 不会告诉你如何组织代码,哪里该存储 state 或 怎么处理事件。”所以开发团队首先要确定 state 的管理架构。

状态管理的学习曲线并不是很陡峭。我们总结下建议:React 初学者首先学习恰当的使用 setState()this.state 。一段时间之后你将会意识到在 React 应用中仅仅使用 setState() 管理状态的问题。当你寻找解决方案时,你会在状态管理库 Mobx 或 Redux 的选择上犹豫。应该选哪个呢?由于 Mobx 是无主张的,使用上可以和 setState() 类似,我建议在小项目中尝试。一旦应用开始变得越来越大,越来越多的人开发时,你应该考虑在 Mobx 上实行更多的限制条件或尝试使用 Redux 。我使用两个库都很享受。即使你最后两个都没使用,了解到状态管理的另一种方式也是有意义的。

尝试另一个状态管理方案?

你可能已经使用了其中一个状态管理方案,但是想考虑另一个?你可以比较现实中的 MobxRedux 应用。我把所有的文件修改都提交到了一个 Pull Request 。在这个 PR 里,项目从 Redux 重构成了 Mobx ,反之亦然,你可以自己实现。我不认为有必要和 Redux 或 Mobx 耦合,因为大部分的改变是和其他任何东西解耦的。

你主要需要将 Redux 的 Actions、Action Creator、 Action Types、Reducer、Global Store 替换成 Mobx 的 Stores 。另外将和 React 组件连接的接口 react-redux 换成 mobx-reactpresenter + container pattern 依然可以执行。你仅仅还要重构容器组件。在 Mobx 中可以使用 inject 获得 store 依赖。然后 store 可以传递 substate 和 actions 给组件。Mobx 的 observer 确保组件在 store 中 observable 的属性变化时更新。

import { observer, inject } from 'mobx-react';

...

const UserProfileContainer = inject(
  'userStore'
)(observer(({
  id,
  userStore,
}) => {
  return (
    <UserProfile
      user={userStore.getUser(id)}
      onUpdateUser={userStore.updateUser}
    />
  );
}));

Redux 的话,你使用 mapStateToPropsmapDispatchToProps 传递 substate 和 actions 给组件。

import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';

...

function mapStateToProps(state, props) {
  const { id } = props;
  const user = state.users[id];

  return {
    user,
  };
}

function mapDispatchToProps(dispatch) {
  return {
    onUpdateUser: bindActionCreators(actions.updateUser, dispatch),
  };
}

const UserProfileContainer = connect(mapStateToProps, mapDispatchToProps)(UserProfile);

这有一篇怎样将 Redux 重构为 Mobx指南。但就像我上面说过的,反过来一样也是可以的。一旦你选择了一个状态管理库,你会知道那并没有什么限制。它们基本上是和你的应用解耦的,所以是可以替换的。

最后思考

每当我看 Redux vs Mobx 争论下的评论时,总会有下面这条:“Redux 有太多的样板代码,你应该使用 Mobx,可以减少 xxx 行代码”。这条评论也许是对的,但没人考虑得失,Redux 比 Mobx 更多的样板代码,是因为特定的设计约束。它允许你推断应用状态即使应用规模很大。所以围绕 state 的仪式都是有原因的。

Redux 库非常小,大部分时间你都是在处理纯 JavaScript 对象和数组。它比 Mobx 更接近 vanilla JavaScript 。Mobx 通过包装对象和数组为可观察对象,从而隐藏了大部分的样板代码。它是建立在隐藏抽象之上的。感觉像是出现了魔法,但却很难理解其内在的机制。Redux 则可以简单通过纯 JavaScript 来推断。它使你的应用更简单的测试和调试。

另外,我们重新回到单页应用的最开始来考虑,一系列的单页应用框架和库面临着相同的状态管理问题,它最终被 flux 模式解决了。Redux 是这个模式的成功者。

Mobx 则又处在相反的方向。我们直接修改 state 而没有拥抱函数式编程的好处。对一些开发者来说,这让他们觉得像双向绑定。一段时间之后,由于没有引入类似 Redux 的状态管理库,他们可能又会陷入同样的问题。状态管理分散在各个组件,导致最后一团糟。

使用 Redux,你有一个既定的模式组织代码,而 Mobx 则无主张。但拥抱 Mobx 最佳实践会是明智的。 开发者需要知道如何组织状态管理从而更好的推断它。不然他们就会想要直接在组件中修改它。

两个库都非常棒。Redux 已经非常完善,Mobx 则逐渐成为一个有效的替代。

彻底搞懂路由跳转:location 和 history 接口

在单页应用中,通常由前端来配置路由,根据不同的 url 显示不同的内容。想要知道这是如何做到的,首先得了解浏览器提供的两大 API:

  1. window.location
  • location.href
  • location.hash
  • location.search
  • location.pathname
  1. window.history
  • history.pushState()
  • history.replaceState()
  • history.go()
  • history.back()
  • history.forward()

window.location

我们先了解 location 对象,location 有很多的属性。我们可以通过改变其属性值修改页面的 url。我们在单页应用中需要做到的是改变 url 不刷新页面,location 接口提供以下两种方式可以做到:

  1. location.href 赋值时只改变 url 的 hash

1

  1. 直接赋值 location.hash

2

而上面的列出其余两个属性 location.search 会直接刷新页面,这个就不解释了。但 location.pathname 照道理来说只改变 hash 应该是可以的,但实际上浏览器会编码这个属性值,所以无法直接赋带 # 号的值。

window.history

history 接口是 HTML5 新增的,它有五个方法可以改变 url 而不刷新页面。

  1. history.pushState()

3

  1. history.replaceState()

4

  1. history.go()

5

上面只演示了三个方法,因为 history.back() 等价于 history.go(-1)history.forward() 则等价于 history.go(1),这三个接口等同于浏览器界面的前进后退。

如何监听 url 的变化

现在我们已经知道如何不刷新页面改变页面的 url。虽然页面没刷新,但我们要改变页面显示的内容。这就需要 js 监听 url 的变化从而达到我们的目的。

我们有两个事件可以监听 url 的改变:

hashchange

hashchange 事件能监听 url hash 的改变。

先要加上事件监听的代码:

window.addEventListener('hashchange', function(e) {
  console.log(e)
})

然后就可以在页面的 console 里愉快的实验了:

6

从上图中我们可以知道不管是通过 location 接口直接改变 hash,还是通过 history 接口前进后退(只是 hash 改变的情况下),我们都可以监听到 url hash 的改变。但这个事件也只能监听 url hash 的变化。所以我们需要一个更强大的事件:popstate

popstate

popstate 事件能监听除 history.pushState()history.replaceState() 外 url 的变化。

先加上事件监听的代码:

window.addEventListener('popstate', function(e) {
  console.log(e)
})

然后又可以在页面的 console 里愉快的实验了:

7

其实不止 history.pushState()history.replaceState() 对 url 的改变不会触发 popstate 事件,当这两个方法只改变 url hash 时也不会触发 hashchange 事件。

hash 模式和 history 模式

我们都知道单页应用的路由有两种模式:hash 和 history。如果我们在 hash 模式时不使用 history.pushState()history.replaceState() 方法,我们就只需要在 hashchange 事件回调里编写 url 改变时的逻辑就行了。而 history 模式下,我们不仅要在 popstate 事件回调里处理 url 的变化,还需要分别在 history.pushState()history.replaceState() 方法里处理 url 的变化。而且 history 模式还需要后端的配合,不然用户刷新页面就只有 404 可以看了😆

所以 hash 模式下我们的工作其实是更简单的,但为什么现在都推荐用 history 模式呢?总不是 hash 模式下的 url 太丑了,毕竟这是个看脸的世界😂

不过 vue-router 在浏览器支持 pushState() 时就算是 hash 模式下也是用 history.pushState() 来改变 url,不知道有没什么深意?还有待研究...

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.