da宗熊

当前位置: 首页 / 笔记

san.js的一次不完全实践

09-14,2018 / da宗熊

Tags: 前端

背景

近几月,新项目需要电脑与手机端,均能正常使用的。考虑到 vue.jsreacti.jsriot.js 等,对 IE 均不友好,于是兴起了使用新框架的念头。

百度的 san.js 逐步浮现在眼前。

它体积小巧,兼容性好(IE6),性能卓越,是一个可靠、可依赖的实现响应式用户界面的解决方案。

上述是它自己吹的哈。小巧和兼容,是肯定的,至于性能?MVVM框架的性能,肯定是有损失的。在性能和维护、效率间权衡了一下,还是投身到MVVM的怀抱中。

传送门: san.js

优点

san.js 有以下几点,是非常吸引人的:

  • 兼容性很好,在移动、电脑端均能放心使用
  • 体积相对较小
  • 有类似冒泡的事件机制,一个dispatch方法,让组件耦合更加松散。子组件 dispatch 事件,一路冒泡,直至有父级组件,对其进行处理。
  • 更加灵活的组件机制,我们甚至可以把动态加载的子组件,挂载到任意组件内。
  • 能灵活使用在任何地方,与任意框架配合使用。自带模板编译,不经 webpack 编译,也可以进行动态创建组件。
  • 有组件反解、服务端渲染等功能呢。
  • 模板语法与 vue.js 语法类似,很低的学习成本;状态管理有类似 flux 的单向流,容易使用。
  • 截止本文发布时,已有 san-storesan-routersan-update 等优秀配套框架,而且还在持续更新中

缺点

在使用的过程中,也遇到一些问题:

  • 取值的表达式,不能支持函数,如 {{ [1, 2, 3].join('x') }},输出将会是 1,2,3,其中的 .join 被完全忽略了
  • 没有全局的混入、拓展等操作,组件间的继承比较麻烦

使用

简单例子

var MyApp = san.defineComponent({
    template: '<p>Hello {{name}}!</p>',

    initData: function () {
        return {
            name: 'San'
        };
    }
});


var myApp = new MyApp();
myApp.attach(document.body);

可以看到,通常情况使用 San 会经过这么几步:

  1. 我们先定义了一个 San 的组件,在定义时指定了组件的 内容模板 与 初始数据 。
  2. 初始化组件对象
  3. 让组件在相应的地方渲染

ESNext 编写,则是:

class myApp extends san.Component {
  // static 关键字,是必须的,包括后面用到的 computed,filters,components  等字段,建议都用 static 关键字
  static template = `<p>Hello {{name}}!</p>`;

  initData: function () {
        return {
            name: 'San'
        };
    }
}

var myApp = new MyApp();
myApp.attach(document.body);

计时器例子

var MyTimer = san.defineComponent({
  template: `<div>当前索引: {{ index }}</div>`,

  initData: function() {
      return { index: 0 };
  },

  attached: function() {
      var data = this.data;
      // 取值和赋值,必须通过 data.set 和 data.get 操作进行
      // 支持表达式赋值、取值,eg: data.get('list[0].name', 'xxx')
      var index = data.get('index');
      setInterval(function() {
          data.set('index', index++);
      }, 1000);
  }
});

var myTimer = new MyTimer({
  // 覆盖掉 initData 中,设置的默认值
  data: {
      index: 10
  }
});
myTimer.attach(document.body);

可以看到,计时器从 10 开始,每秒进行递增。

过滤器、计算属性等

var MyTest = san.defineComponent({
  template: `
    <div>
      姓名: {{ name }}<br/>
      性别: {{ gender | parseSex }}<br/>
      简介: {{ introduce }}
    </div>
  `,

  computed: {
    // 根据 good 的值,计算 introduce 的值
    // 在计算属性内,不允许访问 this.data 外的属性
    introduce: function() {
      var good = this.data.get('good');
      return '你是个' + (good ? '好人' : '坏人');
    }
  },

  filters: {
    // 过滤器的使用与 vue.js 等框架一致
    parseSex: function(val) {
      return val == 1 ? '男' : '女';
    }
  },

  initData: function() {
    return { good: false, name: '', gender: 0 };
  },

  attached: function() {
    var data = this.data;
    setInterval(function() {
      data.set('good', !data.get('good'));
    }, 1000);
  }
});

var myTest = new MyTest({
  data: {
    name: 'san',
    gender: 1
  }
});
myTest.attach(document.body);

效果如下:
jdfw.gif

更多的使用,可查阅官网。下面仅介绍一些 san.js + webpack 使用时,与个人做项目时,遇到的一些问题,以及解决方案。

遇到的问题

san-loader (0.0.7版本)

官方仅在偏僻的角落,以及一些例子中,有提及 san-loader,它的用法,与 vue-loader旧版形式一致,对此熟悉同事,可掠过此小节。

使用如下:

{
    module: {
        loaders: [
            {
                test: /\.san$/,
                loader: 'san-loader'
            }
        ]
    }
}

假设有 index.san 文件:

<template>
    <div class="index">Hello {{ name }}!</div>
</template>

<script>
export default {
  initData() {
    return { name: 'San' };
  }
};
</script>

<style>
.index { color: red; }
</style>

san-loader 的作用下,index.san 会被划分为 htmlcssjs 三块,并且交由 webpack 中,对应的 loader 进行编译、打包。

查看了san-loader 的源码:
Popo截图2018810172619.png
可得知 html 部分交由 html-loader 处理;css 部分交由style-loader处理;js部分,交由babel-loader处理。

PS: 如果无法编译出内容或报错,可查看项目,是否已经引用了 html-loader、style-loader 和 babel-loader

如果要覆盖默认的处理,可按以下格式传入参数:

{
    module: {
        loaders: [
            {
                test: /\.san$/,
                use: [
                    {
                        loader: 'san-loader',
                        options: {
                            // 改写 loaders 中的值,达到覆盖默认 loader 的效果
                            loaders: {
                                html: [ 'html-loader' ],
                                css: [ 'style-loader' ],
                                js: [ 'babel-loader' ]
                            }
                        } // end options
                    }
                ]
            }
        ]
    }
}

细心的同学,可能就注意到,默认的 html-loader 带了一个参数 ?minimize=false。此参数代表了 san-loader 编译出来的 html 内容,均不会被压缩。在实际应用中,些许不友好。我们可以通过重写 options.loaders.html 的值,达到我们压缩 html 的目的。

html-loader: https://www.npmjs.com/package/html-loader

继承

官方有提及,组件间继承,有两种方式: san.inheritsESNext

var CompA = san.defineCompoent({ /* 配置 */ });

// san.inherits
function CompB(options) {
   CompA.call(this, options);
}
san.inerits(CompB, CompA);
MyApp.prototype.template = `<div>内容</div>`;
CompB.prototype.initData = function() {
    return {};
};

// ESNext
class CompC extends CompA {
    /* 可缺省
    constructor(options) {
        super(options);
    }
    */

    static template = `<div>内容</div>`;

    initData() {
        return {};
    }
}

二者对比,推荐使用 ESNext 的继承方式。

官方原话: 由于 ESNext 没有能够编写 prototype 属性的语法,所以 San 对组件定义时的属性支持 static property,通过 ESNext 的 extends 继承时,template / filters / components 属性请使用 static property 的方式定义。

继承一切都好好的,问题就处在了 static 属性上。假如 CompA 现在如下:

class CompA extends san.Component {
    static components = {
        'navbar': Navbar,  // 假设 Navbar 组件,已经存在
    };

    // ....
}

CompB 继承 CompA 时,san.herits 代码如下:
Popo截图2018810181539.png
static 的属性,被完美忽略了。于是 CompB 并没有引入到组件 Navbar

CompC 继承 CompA 时,babel 的继承代码:
Popo截图2018810181746.png
[抽自 babel-runtime/helpers/inherits.js]
可见倒数第二行,subClass.__proto__ = superClass,意图继承 superClass 的静态属性,可惜的是 __proto__ 属性,在当前的 IE 下,全军覆没。[做手机端的同学,木有这个烦恼]

被迫无奈,如果要继承 static 属性,须自己编写继承代码,这是之前写好的代码,可做参考:

'use strict';
import { extend, merge, deepCloneArray } from './extend';
import { typeOf } from './typeof';

/**
 * 类间,静态属性继承
 * es6 的继承,是不会继承 static 属性的,这里修正一下
 *
 * @param {Function} subClass 子类函数
 * @param {Function} superClass 父类函数
 */
export function inheritStatic(subClass, superClass) {
  // 合并 static 的属性,如 superClass.template
  Object.keys(superClass).forEach(key => {
    const subValue = subClass[key];
    const superValue = superClass[key];
    const subType = typeOf(subValue);
    const superType = typeOf(superValue);

    if (subType == superType && subType == 'object') {
      subClass[key] = merge({}, superValue, subValue);
    } else if (subValue == null) {
      switch (superType) {
        case 'object':
          subClass[key] = extend(true, superValue);
          break;
        case 'array':
          subClass[key] = deepCloneArray(superValue);
          break;
        default:
          subClass[key] = superValue;
      }
    }
  });
}

CompC继承CompA后,只需调用 inheritStatic(CompC, CompA),即可继承 CompA 中的静态属性及方法,并且对静态属性的 object 对象,进行了简单的合并操作。

san-store

san-store地址:https://github.com/baidu/san-store
截止本文发布前,san-storesan 配合时,仅建议使用单个 san-store对象,看下面官方的例子:

// 引入的 store 对象,实际是全局声明的
// 所以,后面的所有 store.addAction,实际上,均是捆绑在一个全局对象下
import {store, connect} from 'san-store';
import {builder} from 'san-update';


store.addAction('changeUserName', function (name) {
    return builder().set('user.name', name);
});


let UserNameEditor = san.defineComponent({
    submit() {
        store.dispatch('changeUserName', this.data.get('name'));
    }
});
connect.san({
    name: 'user.name'
})(UserNameEditor);

如果是单页面应用,或者单实例,问题不大。如果需要多个 store 实例,可通过 new Store() 声明新的 Store 对象。然后通过组件继承、或者 $owner 传递的形式,把父级的 store 对象,分发给子组件。

下面,是本人写的一个抽象工具类:

// abstract.js 所有组件,均继承此类

/**
* 获取父组件的 $store 值
*/
function getStore(context) {
    if (context) {
      if (context.$store) {
        return context.$store;
      }
      let owner = context.owner;
      while (owner) {
        if (owner.$store) {
          return owner.$store;
        }
        owner = owner.owner;
      }
    }
    return null;
}

export default class Abstract extends san.Component {
    constructor(options, store) {
        super();

        if (store) {
          // 最顶层组件,传入一个新的 new Store 对象,保存到 this.$store 中
          this.$store = store;
        } else {
          // 子组件找不到 $store 属性,就指向父组件的 $store 对象
          this.$store = getStore(this);
        }
    }
}

san-store/connect

san-storesrc/connect/createConnector中,有这么一段脚本:
xxx.png

如果该组件,已经通过 connect.san 绑定过 san-store 了,将不会重复去初始化 该组件的 actions,本意是很好的。
但是如果我们子组件是继承父组件而来的,而父组件使用过了 connect.san,那么子组件的 connect.san 的绑定,将会失效。

为解决此问题,我们可以把子组件的prototype中的actions值,置为null,然后再调用 connect.san,即可解决

总结

因为体积小,又可实时编译,在做一些小组件、移动端活动的时候,还是非常便捷的,不用考虑兼容 和 体积问题,省下很多功夫。

@2022 da宗熊
Email: 1071093121@qq.com