如何设计一个好的前端架构

前言

什么时候好的前端架构,就是当有新的开发人员接触项目是,不会在理解数据跟踪及其背后体现的UI路径时不至遇上太多的问题。

什么样式好的架构

  • 易于管理
  • 方便理解
  • 规划清晰
  • 更新便捷
  • 组件化程度高
  • 流程方便

页面

页面代表着web应用中不同的目标,页面目录中的文件或者目录,代表着路由路径的目的地。这样,当我们通过路由并拆分出组件时,就能够便捷的径路与页面文件关联起来。
* 仅包含路由入口文件以及其所需要组建的关联
* 入口文件不应该包含完整逻辑,应该见逻辑根据功能拆分至不同的组件
* 规范命名,因为该文件代表着打包后的文件与路由组件

组件

组件越小,就越易于处理。将UI拆分成一个个小的组件。代码越少,我们对代码的掌控能力就越强,调试与必要时的更新就会越简单。可以通过以下方式:
* 将公共组件统一放到一个目录中
* 将每个文件的组件进行分类,确保其中不包含公共代码组件
* 尝试对组件进行概括,以便以后能在不同的场景中使用
* 将彼此相关的组件划分在一起。确保这些组件不会在目录之外的组件中使用

辅助函数

辅助函数应该强大且中立,辅助函数应该与渲染逻辑区分开,仅在需要使用的时候引用。其作用在于:
* 处理特定组件的逻辑
* 与浏览器规范相关
* 处理从后端接收到的数据,使其适用于业务
* 将公共的辅助函数划分到一起,便于管理

API服务

API服务是指负责在参数特定的情况下,调用服务器以获取数据的代码。我们不应该直接从UI逻辑中调用服务。因为如果我们需要在很多位置实现相同的API调用,name对不同位置进行修改迭代将变得非常困难。
* 应该将API服务进行封装,单独做一个服务来实现
* 应将从服务器接收的数据直接返回给组件
* 应该能接收配置或者变量等,作为API服务的必要参数进行传递

Config

Config 当中应包含web应用运行所在的环境具体配置。确保将配置与实际代码拆分出来。
* 使用不同的文件对应不同的环境类型
* 根据获取不同的资源类型而有所不同

路由

路由是保障web应用使用体验的主要方式,路由决定这我们在应用中需要显示的不同页面的URL格式或者模式。
* 路由的命名应该尽可能简短
* 尽可能保持路由的正确顺序

Static文件

Static文件是指未包含在逻辑当中的文件。
* 应该根据其类型进行分组
* 尽可能降低文件体积

其他

  • 如果是在用npm管理的话,package.json 应该有完善的相关命令,来保证开发人员流程畅通
  • readme 要写的足够详尽,因为一个开发如果要想了解一个项目的话,都会先阅读readme

以上就是我总结的一些想法,现在前端发展迅速,整个设局对于项目架构的思路也是日新月异,我只是希望我写的这些能起到一些帮助。

双向绑定Proxy比defineproperty优劣

前言

双向绑定其实已经是一个老掉牙的问题了,只要涉及到MVVM框架就不得不谈的知识点,但它毕竟是Vue的三要素之一

Vue三要素

  • 响应式: 例如如何监听数据变化,其中的实现方法就是我们提到的双向绑定
  • 模板引擎: 如何解析模板
  • 渲染: Vue如何将监听到的数据变化和解析后的HTML进行渲染

可以实现双向绑定的方法有很多,KnockoutJS基于观察者模式的双向绑定,Ember基于数据模型的双向绑定,Angular基于脏检查的双向绑定,本篇文章我们重点讲面试中常见的基于数据劫持的双向绑定。

常见的基于数据劫持的双向绑定有两种实现,一个是目前Vue在用的Object.defineProperty,另一个是ES2015中新增的Proxy,而Vue的作者宣称将在Vue3.0版本后加入Proxy从而代替Object.defineProperty,通过本文你也可以知道为什么Vue未来会选择Proxy。

严格来讲Proxy应该被称为『代理』而非『劫持』,不过由于作用有很多相似之处,我们在下文中就不再做区分,统一叫『劫持』。

基于Object.defineProperty双向绑定的特点

关于Object.defineProperty的文章在网络上已经汗牛充栋,我们不想花过多时间在Object.defineProperty上面,本节我们主要讲解Object.defineProperty的特点,方便接下来与Proxy进行对比。

极简版的双向绑定

我们都知道,Object.defineProperty的作用就是劫持一个对象的属性,通常我们对属性的getter和setter方法进行劫持,在对象的属性发生变化时进行特定的操作。
我们就对对象obj的text属性进行劫持,在获取此属性的值时打印’get val’,在更改属性值的时候对DOM进行操作,这就是一个极简的双向绑定。

const obj = {};
Object.defineProperty(obj, 'text', {
  get: function() {
    console.log('get val'); 
  },
  set: function(newVal) {
    console.log('set val:' + newVal);
    document.getElementById('input').value = newVal;
    document.getElementById('span').innerHTML = newVal;
  }
});

const input = document.getElementById('input');
input.addEventListener('keyup', function(e){
  obj.text = e.target.value;
})

Object.defineProperty的缺陷

Object.defineProperty的第一个缺陷,无法监听数组变化。 然而Vue的文档提到了Vue是可以检测到数组变化的,但是只有以下八种方法,vm.items[indexOfItem] = newValue这种是无法检测的。

push()
pop()
shift()
unshift()
splice()
sort()
reverse()

其实作者在这里用了一些奇技淫巧,把无法监听数组的情况hack掉了,以下是方法示例。

const aryMethods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'];
const arrayAugmentations = [];

aryMethods.forEach((method)=> {

    // 这里是原生Array的原型方法
    let original = Array.prototype[method];

   // 将push, pop等封装好的方法定义在对象arrayAugmentations的属性上
   // 注意:是属性而非原型属性
    arrayAugmentations[method] = function () {
        console.log('我被改变啦!');

        // 调用对应的原生方法并返回结果
        return original.apply(this, arguments);
    };

});

let list = ['a', 'b', 'c'];
// 将我们要监听的数组的原型指针指向上面定义的空数组对象
// 别忘了这个空数组的属性上定义了我们封装好的push等方法
list.__proto__ = arrayAugmentations;
list.push('d');  // 我被改变啦! 4

// 这里的list2没有被重新定义原型指针,所以就正常输出
let list2 = ['a', 'b', 'c'];
list2.push('d');  // 4

由于只针对了八种方法进行了hack,所以其他数组的属性也是检测不到的,其中的坑很多,可以阅读上面提到的文档。
我们应该注意到在上文中的实现里,我们多次用遍历方法遍历对象的属性,这就引出了Object.defineProperty的第二个缺陷,只能劫持对象的属性,因此我们需要对每个对象的每个属性进行遍历,如果属性值也是对象那么需要深度遍历,显然能劫持一个完整的对象是更好的选择。

Object.keys(value).forEach(key => this.convert(key, value[key]));

Proxy实现的双向绑定的特点

Proxy在ES2015规范中被正式发布,它在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写,我们可以这样认为,Proxy是Object.defineProperty的全方位加强版

Proxy可以直接监听对象而非属性

我们还是以上文中用Object.defineProperty实现的极简版双向绑定为例,用Proxy进行改写。

const input = document.getElementById('input');
const p = document.getElementById('p');
const obj = {};

const newObj = new Proxy(obj, {
  get: function(target, key, receiver) {
    console.log(`getting ${key}!`);
    return Reflect.get(target, key, receiver);
  },
  set: function(target, key, value, receiver) {
    console.log(target, key, value, receiver);
    if (key === 'text') {
      input.value = value;
      p.innerHTML = value;
    }
    return Reflect.set(target, key, value, receiver);
  },
});

input.addEventListener('keyup', function(e) {
  newObj.text = e.target.value;
});

我们可以看到,Proxy直接可以劫持整个对象,并返回一个新对象,不管是操作便利程度还是底层功能上都远强于Object.defineProperty。

Proxy可以直接监听数组的变化

当我们对数组进行操作(push、shift、splice等)时,会触发对应的方法名称和length的变化,我们可以借此进行操作,以上文中Object.defineProperty无法生效的列表渲染为例。

const list = document.getElementById('list');
const btn = document.getElementById('btn');

// 渲染列表
const Render = {
  // 初始化
  init: function(arr) {
    const fragment = document.createDocumentFragment();
    for (let i = 0; i < arr.length; i++) {
      const li = document.createElement('li');
      li.textContent = arr[i];
      fragment.appendChild(li);
    }
    list.appendChild(fragment);
  },
  // 我们只考虑了增加的情况,仅作为示例
  change: function(val) {
    const li = document.createElement('li');
    li.textContent = val;
    list.appendChild(li);
  },
};

// 初始数组
const arr = [1, 2, 3, 4];

// 监听数组
const newArr = new Proxy(arr, {
  get: function(target, key, receiver) {
    console.log(key);
    return Reflect.get(target, key, receiver);
  },
  set: function(target, key, value, receiver) {
    console.log(target, key, value, receiver);
    if (key !== 'length') {
      Render.change(value);
    }
    return Reflect.set(target, key, value, receiver);
  },
});

// 初始化
window.onload = function() {
    Render.init(arr);
}

// push数字
btn.addEventListener('click', function() {
  newArr.push(6);
});

很显然,Proxy不需要那么多hack(即使hack也无法完美实现监听)就可以无压力监听数组的变化,我们都知道,标准永远优先于hack。

Proxy的其他优势

Proxy有多达13种拦截方法,不限于apply、ownKeys、deleteProperty、has等等是Object.defineProperty不具备的。
Proxy返回的是一个新对象,我们可以只操作新的对象达到目的,而Object.defineProperty只能遍历对象属性直接修改。
Proxy作为新标准将受到浏览器厂商重点持续的性能优化,也就是传说中的新标准的性能红利。
当然,Proxy的劣势就是兼容性问题,而且无法用polyfill磨平,因此Vue的作者才声明需要等到下个大版本(3.0)才能用Proxy重写。

lerna管理好你的packages

前言

最近在研究包管理相关的东西,于是发现了lerna

什么是Lerna

Lerna是一个管理多个node模块的工具,是Babel自己用来维护自己的Monorepo并开源出的一个项目。Lerna现在已经被很多著名的项目组织使用。

Monorepo

Monorepo的全程是monolithic repository,即单体式仓库,与之对应的是Multirepo(multiple repository)。以下是两者的区别。

Monorepo是把所有相关的module放在一个仓库中进行管理,每个module独立发布,优缺点总结如下:

优点
* 版本更新简单便捷,core repo 以及各模块版本发生变化后可以简便的同步更新其他对其有依赖的module
* 管理简单,issue readme pr 都放在一起维护
* changelog 方便维护,所有的修改基于一个commit列表

缺点
* 仓库体积增长迅速,随着module的增多,仓库的体积也会变得庞大。
* 自由度不高,对统一的配套工具要求较高,要适配每个module的要求。

Multirepo相对来说比较传统,没一个组件都单独用一个仓库来维护管理,优缺点如下:

优点
* 每个模块可自行管理,自由度高,可自行选择构建工具等相关工具。
* 每个仓库体积较小

缺点
* 项目管理混乱,issue 经常出现针对其他module的问题,需要更多精力维护。
* changlog 无法关联,无法很好的自动关联各个 module 与 core repo 之间的变动联系
* 版本更新繁琐,如果 core repo 的版本发生了变化,需要对所有的 module 进行依赖 core repo 的更新
* 测试复杂,对多个相关联 module 测试繁琐

Babel为了解决上面的问题,于是诞生了Lerna,Lerna可以通过git和npm帮助我们来优化管理Monorepo的工作流,同时较少开发和构建环境中对大量依赖包复制的时间和控件需求。

初始化一个Lerna工程

一个基本的Lerna仓库结构如下

.
├── lerna.json
├── package.json
└── packages
    ├── module-1
    ├── module-2
    └── module-3

首先要创建一个Lerna项目
我们要全局安装Lerna

npm i -g lerna
mkdir lerna-demo && cd $_
lerna init

Lerna提供两种不同的方式来管理你的项目:Fixed或Independent,默认采用Fixed模式,如果你想采用Independent 模式,只需在执行init命令的时候加上–independent或-i参数即可。

Fixed/Locked 模式(默认)

固定模式下Lerna项目在单一版本线上运行。版本号保存在项目根目录下lerna.json文件中的version下。当你运行lernapublish时,如果一个模块自上次发布版本以后有更新,则它将更新到你将要发布的新版本。这意味着你在需要发布新版本时只需发布一个统一的版本即可。

Independent 模式(–independent)

独立模式下Lerna允许维护人员独立地的迭代各个包版本。每次发布时,你都会收到每个发生更改的包的提示,同时来指定它是patch,minor,major还是自定义类型的迭代。

Lerna 实践

为了能够使lerna发挥最大的作用,根据这段时间使用Lerna的经验,总结出一个最佳实践。下面是一些特性。

  • 采用Independent模式
  • 根据Git提交信息,自动生成changelog
  • eslint规则检查
  • prettier自动格式化代码
  • 提交代码,代码检查hook
  • 遵循semver版本规范

工具整合

在这里引入的工具都是为了解决一个问题,就是工程和代码的规范问题。
* husky
* lint-staged
* prettier
* eslint

vue-cli3定制脚手架命令行工具实践

前言

目前大多数脚手架项目都是在用yeoman开发,但是逐渐暴露除了一些问题:
* 团队中的脚手架都是采用个人维护,出于维护成本的考虑,脚手架的更新会相对较慢,滞后于前端技术的发展。
* 脚手架生成的前端项目的webpack配置,也由于需求不同导致参差不齐。
vue-cli就很大程度上解决了上面的问题,它提供了最基础的配置,大家可以开箱即用,同时也可以最大程度上的定制化,也就是之前讲到的预设

原理

原理很简单,就是将vue-cli命令进行封装。制定一个特殊命令,将定制好的 preset 交给 vue-cli处理,而其他情况则完全透转给 vue-cli。

实践

命令行工具的核心就是,创建个命令 create ,核心就是当执行 my-cli create demo 的时候,就会把定义好的preset 传给 vue-cli处理。

#!/usr/bin/env node
const path = require('path');
const preset = path.resolve(__dirname,'my-preset');

const program = require('commander');
const execa = require('execa');

program
  .version(require('./package').version)
  .description('基于vue-cli的定制脚手架')
  .usage(' [options]');

program
  .command('create ')
  .description('使用定制preset创建vue-cli项目')
  .action(function (project) {
    //最核心就是这里
    let command = `vue create ${project} --preset ${preset}`;
    const child = execa.shell(command, {
        stdio: 'inherit'
    });
  })

//除了专有的create命令,其他的命令都转交给vue-cli
program
  .command('*')
  .action(function(){
      let command = process.argv.slice(2);
      command.unshift('vue');

      const child = execa.shell(command.join(' '),{
          stdio:'inherit'
      });
  });

program.parse(process.argv);

这样在想要使用自己的预设的时候,就使用自己定制的命令,同时也不影响vue-cli本来的命令。

vue-cli3项目预设实践

前言

vue-cli3已经出了有一段时间了,现在4.0 都已经进入了alpha阶段。虽然乍一看3.0貌似舍弃了2.0时代的模板功能,但其实没有,3.0可以通过预设插件的形式来做到模板。毕竟模板在很多业务场景下还是很有必要的功能。

插件和Preset

插件

插件是3.0新引入的概念,基于插件的架构可以使得vue-cli变得更加灵活(说明文档)。

Preset

可以翻译为 预设

一个包含创建新项目所需预定义选项和插件的 JSON 对象
还可以理解为一套预置的项目模板,也就是本文要讲的。
使用vue create 创建过项目的小伙伴应该都记得,在创建完成后 CLI 会提示是否保存为一个 preset,这里第一条指的就是要保存的那个对象。如果你保存过,下面的命令就能看到之前保存的 preset。

cat ~/.vuerc
每个 preset.json 大概是这么个格式:

{
  "useConfigFiles": true,
  "plugins": {...},
  "configs": {
    "vue": {...},
    "postcss": {...},
    "eslintConfig": {...},
    "jest": {...}
  }
}

说明

由于本文主要讲的是 Preset,如果大家想了解插件可以看看文档,以后有机会可以再给大家讲下。

Prompts

本质上是一个对话配置文件,vue 内置插件 和 第三方插件 的这个文件的写法是不一样的。我们只要记得:
它是一个 Inquirer.js 的 问题 的数组

module.exports = [
  {
    name: "vuex",
    type: "confirm",
    message: `是否需要使用 vuex`,
    default: false
  },
  {
    name: "elementUI",
    type: "confirm",
    message: `element-ui`,
    default: false
  }
];

上边就是两个很简单的示例
1,询问是否需要vuex
2,是否需要element-ui

Generator

可以叫它生成器,它导出一个函数,该函数接收三个参数

1,api : 一个 GeneratorAPI 实例
2,options: 可以先简单理解为 prompts 问题数组的用户输入 组合成的选项对象
3,rootOptions: 整个 preset.json 对象

module.exports = (api, options, rootOptions) => {
  // 安装一些基础公共库
  api.extendPackage({
    dependencies: {
      "axios": "^0.18.0",
      "lodash": "^4.17.10",
      "keymirror": "^0.1.1"
    },
    devDependencies: {
      "mockjs": "^1.0.1-beta3"
    }
  });

  // 安装 vuex
  if (options.vuex) {
    api.extendPackage({
      dependencies: {
        vuex: '^3.0.1'
      }
    });

    api.render('./template/vuex');
  }

  // 安装 element-ui 库
  if (options.elementUI) {
    api.extendPackage({
      devDependencies: {
        "element-ui": "^2.4.6"
      }
    });
  }

  // 公共基础目录和文件
  api.render('./template/default');

  // 配置文件
  api.render({
    './.eslintrc.js'     : './template/_eslintrc.js',
    './.gitignore'       : './template/_gitignore',
    './.postcssrc.js'    : './template/_postcssrc.js'
  });
}

核心 api:

1,api.extendPackage : 负责给初始化项目中的 package.json 添加额外依赖并安装
2,api.render : 负责将模板项目中提前定义好的目录和文件拷贝到初始化的项目中
3,api.postProcessFiles : 负责具体处理模板项目中的文件

对于 api.render 需要注意几点:
1,拷贝目录的话,直接传地址字符串,render 函数会将你所传目录内的所有文件覆盖初始化项目中 src 目录下的文件(我的测试结果是限于两层目录)
2,拷贝文件的话,直接传入一个 object,其中 key 对应初始化项目中的目标位置,value 对应模板项目中的文件位置
3,当你需要创建一个以 . 开头的文件时,模板项目中需要用 _ 替代 .

最后还有很重要的一点,vue-cli 3 在拷贝文件时使用的是 EJS 模板去实现的,所以开发者是可以在任意文件中使用 EJS 语法去做更细粒度的控制。

结尾

以上就是在做预设过程中需要注意的一些地方,具体模板的内容就要根据大家的业务需求自己去进行扩充了。

如何用 JS 实现 JSON.parse

前言

下面我们来对JSON之父写的 ployfill里边提供的是那种方式分析一下。

Eval

第一种方式最简单,也最直观,就是直接调用 eval,代码如下:

var json = '{"a":"1", "b":2}';
var obj = eval("(" + json + ")");  // obj 就是 json 反序列化之后得到的对象

因为 JSON 脱胎于 JS,同时也是 JS 的子集,所以能够直接交给 eval 运行。
PS: 然而,通常我们都说 eval 是邪恶的,尽量不要使用.
ok,回到上面,我们像新手一样直接调用 eval,会不会出问题呢?
答案当然是会,这里有 XSS 漏洞。触发条件:参数 json 并非真正的 JSON 数据,而是可执行的 JS 代码。
那么,该如何规避这个问题呢?
大神 Douglas Crockford 给我们做了示范:对参数 json 做校验,只有真正符合 JSON 格式,才能调用 eval,具体就是下面这几个正则匹配。

// We split the second stage into 4 regexp operations in order to work around
// crippling inefficiencies in IE's and Safari's regexp engines. First we
// replace the JSON backslash pairs with "@" (a non-JSON character). Second, we
// replace all simple value tokens with "]" characters. Third, we delete all
// open brackets that follow a colon or comma or that begin the text. Finally,
// we look to see that the remaining characters are only whitespace or "]" or
// "," or ":" or "{" or "}". If that is so, then the text is safe for eval.

var rx_one = /^[\],:{}\s]*$/;
var rx_two = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g;
var rx_three = /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g;
var rx_four = /(?:^|:|,)(?:\s*\[)+/g;

if (
    rx_one.test(
        json
            .replace(rx_two, "@")
            .replace(rx_three, "]")
            .replace(rx_four, "")
    )
) {
    var obj = eval("(" +json + ")");
}

对于上面复杂的正则不知道大家是不是感觉一头雾水,反正我是看的满脸问号。。。
所以我们还是用正则工具Regexper来给我们分析下
avatar
avatar
avatar
上面还是有两个需要注意的地方
1. 其中有一段

Third, we delete all open brackets that follow a colon or comma or that begin the text.

表面上看起来要删除 open brackets 开括号(,而实际上正则 rx_four 匹配删除的却是[,这是为什么呢?因为中英文语义的不同。在中文里,开括号一般指(,而在英文里开括号一般指[,其间细微差别需要知道。
2.看 rx_three,里面有(?:)结构,这是正则的不捕获分组,具体可以参考这里。使用不捕获分组的原因:要解析的 json 有可能是一个很大的 JSON,如果匹配到的每个 token 都缓存起来的话,那么对内存的消耗是巨大的,而这里我们只想替换字符,并不需要知道都匹配到了哪些字符。

递归

除了eval那样吧JSON字符串全部塞进去的方法,我们还可以手动字符扫描然后进行判断,这就是递归。

// 调用核心的 next 函数,逐个读取字符
var next = function (c) {

// If a c parameter is provided, verify that it matches the current character.

    if (c && c !== ch) {
        error("Expected '" + c + "' instead of '" + ch + "'");
    }

// Get the next character. When there are no more characters,
// return the empty string.

    ch = text.charAt(at);
    at += 1;
    return ch;
};

所谓“递归”,就是重复调用 value 函数。

value = function () {

// Parse a JSON value. It could be an object, an array, a string, a number,
// or a word.

    white();
    // 根据当前字符是什么,我们便能推导出后面应该接的是什么类型
    switch (ch) {
        case "{":
            return object();
        case "[":
            return array();
        case "\"":
            return string();
        case "-":
            return number();
        default:
            return (ch >= "0" && ch <= "9")
                ? number()
                : word();
    }
};

以 ‘{“a”:”1″, “b”:2}’ 为例,程序大致逻辑是:启动 → 首次调用 value() → 发现是 { → 原来是对象,走 object() → 通过 string() 得到 key 值为 “a” → 读取到冒号,哦,后面可能是对象、数组、布尔值等等,具体是什么,还得再次调用 value() 才知道 → ….

状态机

状态机名字起得很抽象,应用也非常广泛,比如正则引擎、词法分析,甚至是字符串匹配的 KMP 算法都能用它来解释。它代表着一种本质的逻辑:在 A 状态下,如果输入 B,就会转移到 C 状态。

那么,状态机与 JSON 字符串的解析有什么关系呢?→ JSON 字符串是有格式规范的,比如 key 和 value 之间用冒号隔开,比如不同 key-value 对之间用逗号隔开……这些格式规范可以翻译成状态机的状态转移,比如“如果检测到冒号,那么意味着下一步可以输入 value” 等等。还是以'{“a”:”1″, “b”:2}’为例,我们来看看对这个 JSON 字符串进行解析时,状态机都流经了哪些状态。
avatar
另外,这第三种实现方式,代码看起来非常的规整,是因为其广泛地应用了访问者模式,比如

var string = {   // The actions for string tokens
    go: function () {
        state = "ok";
    },
    firstokey: function () {
        key = value;
        state = "colon";
    },
    okey: function () {
        key = value;
        state = "colon";
    },
    ovalue: function () {
        state = "ocomma";
    },
    firstavalue: function () {
        state = "acomma";
    },
    avalue: function () {
        state = "acomma";
    }
};

额外的骚操作

在查找上面三种的资料的同时也发现了个非主流的方式

Function

语法

var func = new Function(arg1, arg2, ..., functionBody);

例子

var add = new Function('a, b', 'return a+b;');
console.log( add(2, 3) );    // 5

在转换JSON的实际应用中,只需要这么做。

var jsonStr = '{ "age": 20, "name": "jack" }',
    json = (new Function('return ' + jsonStr))();

原因是eval 与 Function 都有着动态编译js代码的作用,但是在实际的编程中并不推荐使用。如果可以,请用更好的方法替代。
在一些特殊的运用场合,也有一些合理运用的实践。比如模板解析等。

后记

以上就是我查到的一些关于 JSON.parse 的一些实现,我也是翻阅了资料后才知道原来还有这么多讲究之处。虽然在我们平常写业务中不会用到,但是其中的一些原理还是想和大家分享下。
谢谢

vue虚拟DOM渲染成Canvas

前言

在刷github的时候偶然发现一个有意思的库vnode2canvas,因为关于转canvas我们通常都会用到html2canvas。我们知道vue是通过vnode实现渲染工作,所以vnode2canvas实现了vnode -> canvas,简化了html2canvas vnode -> html -> canvas。

背景

下面是关于canvas的概述

canvas是一种立即模式的渲染方式,不会存储额外的渲染信息。从而Canvas润徐直接发送绘图命令到GPU。但若用它来构建用户界面,需要进行一个更高层次的抽象。例如一些简单的处理,比如当绘制一个异步加载的资源到一个元素上会出现问题,如在图片上绘制文本。在HTML中,由于元素存在顺序,以及css中存在z-index,因此是很容易实现的。dom渲染是一种保留模式,保留模式是一种声明性的API,用于维护绘制到其中对象的层次结构中。保留模式API的优点是,对于你的应用程序,他们通常更容易构建复杂的场景,例如DOM。通常这都带来性能成本,需要额外的内存来保存场景和更新场景,这可能会减慢速度。

源码分析

vnode处理

Vue.mixin({
  // ...
  created() {
    if (this.$options.renderCanvas) {
      // ...
      // 监听vnode中引用的变化,重新渲染
      this.$watch(this.updateCanvas, this.noop)
      // ...
    }
  },
  methods: {
    updateCanvas() {
      // 模拟Vue render 函数
      // 寻找实例中定义的 renderCanva 方法,并传入 createElement 方法
      let vnode = this.$options.renderCanvas.call(this._renderProxy, this.$createElement)
    }
  }
})

Vue通过render函数,传入createElement方法创造vnode,这里就是加了个监听函数,混入到Vue实例中。

canvas元素处理

同时作者还对render的vnode做了额外的约束,比如:

view/scrollView/scrollItem --> fillRect
text --> fillText
image --> drawImage

其中这些元素类分别都继承于一个Super类,并且由于它们各有不同的展示方式,因此它们分别实现自己的draw方法,做定制化的展示。

调用

renderCanvas(h) {
  return h('view', {
     style: {
       left: 10,
       top: 10,
       width: 100,
       height: 100
     }
  })
}

通过这样绘制 canvas 布局最基础的写法是为canvas 元素传入一系列坐标点和相关的基础宽高。

通过作者相关文章,了解到作者还有别的方案去处理,因为上面写法还是有些不方便,其一就是写一个webpack loader 加载外部css

const css = require('css')
module.exports = function (source, other) {
  let cssAST = css.parse(source)
  let parseCss = new ParseCss(cssAST)
  parseCss.parse()
  this.cacheable();
  this.callback(null, parseCss.declareStyle(), other);
};

class ParseCss {
  constructor(cssAST) {
    this.rules = cssAST.stylesheet.rules
    this.targetStyle = {}
  }

  parse () {
    this.rules.forEach((rule) =&gt; {
      let selector = rule.selectors[0]
      this.targetStyle[selector] = {}
      rule.declarations.forEach((dec) =&gt; {
        this.targetStyle[selector][dec.property] = this.formatValue(dec.value)
      })
    })
  }

  formatValue (string) {
    string = string.replace(/"/g, '').replace(/'/g, '')
    return string.indexOf('px') !== -1 ? parseInt(string) : string
  }

  declareStyle (property) {
    return `window.${property || 'vStyle'} = ${JSON.stringify(this.targetStyle)}`
  }
}

简单的来说:主要也就是将 css 文件转成AST语法树,之后再对语法树做转换,转成canvas需要的定义形式。并以变量的形式注入到组件中。

结语

上面就是我与这个库的理解,这个库在一些卡片生成的业务场景下还是有很大的应用空间。

下面是例子地址:

JS-常见内存泄漏处理

前言

计算机语言,比如C语言中,有专门的内存管理接口,像malloc()free()。开发人员使用这些方法从操作系统分配和释放内存。
同理,在js中创建事物(对象,字符串等)时分配内存,并且在不使用的时候“自动”释放,这个过程称为垃圾回收。这种看似“自动”的释放资源其本质是造成混乱的根源,并给js开发提供了错误的印象,他们可以选择不关心内存管理。这其实是一个大错误。

自动GC的问题

尽管GC很方便,但是我们不知道GC会在什么时候运行,这就意味着我们在使用了大量内存的时候,GC没有运行,或者说GC无法回收这些内存的情况下,程序可能出现假死,这个就需要我们在程序中手动触发内存回收。

什么是内存泄漏

下面是wiki的解释

在计算机科学中,内存泄漏指由于疏忽或错误造成程序未能释放已经不再使用的内存。内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,导致在释放该段内存之前就失去了对该段内存的控制,从而造成了内存的浪费。

四种常见的内存泄漏案例及处理

1.全局变量

function foo(arg) {
  bar = "some text";
}

在js中处理未被声明的变量。在上面代码中的bar会被定义到全局对象中,在浏览器中就是window上。页面中的全局变量,只有在页面关闭后才会被销毁。所以这样会导致内存泄漏,虽然在例子中是简单的字符串,但是如果是实际代码中可能会更加的严重的情况。

下面是另外一种情况

function foo() {
  this.var = "some text";
}
foo();

在这种情况下调用foo,this被指向了全局变量window,意外的创建了全局变量。

上面是一些意外情况下被定义的全局变量,我们平时也有一些我们明确定义的全局变量,如果使用这些变量用来缓存的话,要在使用后,对其重新赋值为null。

2.未销毁的定时器和回调函数

如果项目中使用了观察者模式,都会提供回调方法,来调用一些回调函数。要记住得回收这些回调函数。比如

var serverData = loadData()
setInterval(function() {
  var renderer = document.getElementById('renderer');
  if (renderer) {
    renderer.innerHTML = JSON.stringify(serverData);
  }
}, 5000);

上面代码中如果后续的renderer元素被移除,整个定时器实际上没有任何作用。如果你没有回收定时器,它依旧有效,不但定时器无法被内存回收,定时器函数中的依赖也无法回收。

3.闭包

我们在日常开发中,经常会用到闭包,一个内部函数,有访问包含其的外部函数中的变量,下面情况中,闭包也会造成内存泄漏。

var theThing = null;
var replaceThing = function() {
  var originalThing = theThing;
  var unused = function() {
    if (originalThing) console.log('hi');
  };
  theThing = {
    longStr: new Array(1000000).join('*'),
    someMethod: function() {
      console.log('message');
    }
  };
};
setInterval(replaceThing, 1000);

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

3.DOM引用

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

var elements = {
  image: document.getElementById('image')
};
function doStuff() {
  elements.image.src = 'http://example.com/image_name.png';
}
function removeImage() {
  document.body.removeChild(document.getElementById('image'));
}

上面代码中,及时我们队image元素进行了移除,但是仍然有对image元素的引用,依然无法进行内存回收。

还有需要注意的一点是,对于一个DOM树的叶子节点的引用,比如 如果我们引用了一个表格中的td元素,一旦在DOM中删除了整个表格,我们直观的觉得内存回收还应该回收了除了被引用的td外的其他元素,但事实上,这个td元素是整个表格的子元素,并保留着对于其父元素的引用,这就会导致对于整个表格,都无法进行内存回收,所以我们要小心处理对于DOM元素的引用。

结尾

虽然现代浏览器以及我们用的框架帮我们做了很多优化,但是还是希望在我们平时代码中,尽量避免内存泄漏。

参考文章

How JavaScript works: memory management + how to handle 4 common memory leaks

JSON Schema在Vue中的实践(二)

前言

上一篇我们讲到了如何通过schema 生成表单,但只是生成表单还是远远不够的,下面我就为大家讲下如何数据绑定以及组件复用。

数据绑定

我们目前已经有了一个表单,但是没有办法将数据绑定。首先想到的可能是使用组建的v-model,添加一个value属性。

<input
  type="text"
  :name="name"
  v-model="value"
  :placeholder="placeholder"
/>

这样写的话回报错误

[Vue warn]: Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value. Prop being mutated: "value"

found in

---> <TextInput> at /src/components/TextInput.vue
       <FormsDemo> at /src/App.vue
         <Root>

因为虽然Vue提供了语法糖,使得组件可以实现双向绑定,但是框架还是偏向单项数据流。如果我们试图修改父组件的数据,Vue还是会向我们发出警告。

所以我们根据vue的官方文档中的介绍来解决

<input v-model="something">

<input  
  v-bind:value="something"
  v-on:input="something = $event.target.value"
>

以上两种写法是等同的
通过第二种我们就可以实现把值提供给子组件,并且让父组件知道值的更新。

我们通过绑定到value并发出@input事件来通知父组件值已经发生变化,从而完成此操作。

以下为实现代码

// 子组件
<div>
  <label>{{label}}</label>
  <input type="text"
    :name="name"
    :value="value"
    @input="$emit('input', $event.target.value)"
    :placeholder="placeholder"
  >
</div>
// 父组件
<component v-for="(field, index) in schema"
  :key="index"
  :is="field.fieldType"
  v-model="formData[field.name]"
  v-bind="field">
</component>  

这样父组件提供绑定值,同时也负责处理绑定到它自己的组件状态。

现在我们算是基本完成了生成表单的工作,既然是组件我们就要做到可复用

可复用性

对于一个表单生成器,我们肯定希望将数据作为prop传递过去,然后在组件之间建立数据绑定。

比如这样

<form-generator :schema="schema" v-model="formData">  
</form-generator>

这样简化了父组件的复杂度。

我们创建了名为FormGenerator的组件。

代码如下

<component
  v-for="(field, index) in schema"
  :key="index"
  :is="field.fieldType"
  :value="formData[field.name]"
  @input="updateForm(field.name, $event)"
  v-bind="field">
</component>

我们将v-model改为了:value,使用@input处理事件,添加value和props上。

这样我们就有了一个可复用的表单生成器了。

结尾

由于马上就要过年了,时间比较匆忙,最终的代码我之后会整理出来,之后发给大家,实在抱歉,提前给大家拜个早年。

JSON Schema在Vue中的实践(一)

上一篇相信大家已经对于JSON Schema有了一定的认识,这篇将向大家展示如何利用vue的动态组件根据schema来生成一个动态的表单生成器,这样在管理后台,设置中心等类似的场景中,你完全可以利用这种思路来更效率地开发界面。

前言

vue要实现动态组件需要用到“““的内置组件,相关使用方法可以查看文档

比如我们要是切换组件可以这样实现:

<component :is="componentType">  

可能上边的还不够具体,这样让我们弄一个更具体的例子。我们创建两个组件叫做ComponentOne和ComponentTwo,因为两个组件十分相近,一下就不重复展示了。

<template>  
  <div>Component One</div>
</template>  
<script>  
export default {  
  name: 'ComponentOne',
}
</script>

下面就是一个能在两个组件之间切换的实例,我们在index.vue中切换组件。

<template>
  <div>
    <button @click="showWhich = 'ComponentOne'">Show Component One</button>  
    <button @click="showWhich = 'ComponentTwo'">Show Component Two</button>

    <component :is="showWhich"></component>  
  </div>
</template>

<script>
import ComponentOne from './components/ComponentOne.vue';
import ComponentTwo from './components/ComponentTwo.vue';

export default {
  name: 'app',
  components: {
    ComponentOne,
    ComponentTwo,
  },
  data() {
    return {
      showWhich: 'ComponentOne',
    };
  },
};
</script>

上面代码可见,showWhich属性是在组件创建后的属性名,我们通过两个按钮来切换这两个组件。

##实现
现在我们了解动态组件的基本知识,现在我们就可以开始构建表单生成器了。

我们先从一个简单的表单开始:
* 文本输入框
* 一个选项列表

我们schema是这样的:

schema: [
  {
    fieldType: 'SelectList',
    name: 'title',
    multi: false,
    label: 'Title',
    options: ['A', 'B', 'C', 'D'],
  },
  {
    fieldType: 'TextInput',
    placeholder: 'First Name',
    label: 'First Name',
    name: 'firstName',
  },
]

我们的输入框组件可以这么实现:

<template>
  <label>{{label}}</label>
  <input type="text"
    :name="name"
    placeholder="placeholder">
</template>




export default {
  name: 'TextInput',
  props: ['placeholder', 'label', 'name'],
};



选择框:

注 编写后显示有问题 所以截图 非常抱歉

这样我们要根据schema生成表单需要这样写:

<component
  v-for="(field, index) in schema"  
  :key="index"
  :is="field.fieldType"
  v-bind="field">
</component>  

效果如下

Edit vue-components

##总计
以上就是通过vue动态组件实现简单的表单,但是其中还是存在一些没有晚上的细节,比如数据绑定和事件绑定,由于时间关系我们下次有时间再向大家继续讲解。
谢谢