使用

为了方便浏览器直接运行,文档中的例子尽量使用es5编写,当然你也可以使用es6语法,如有必要,文档中会标明es6写法的差异

Hello Intact

下面通过一个简单的例子,来介绍Intact组件的使用方法。

  1. var App = Intact.extend({
  2. defaults: function() {
  3. return {
  4. name: 'Intact'
  5. };
  6. },
  7. template: '<div>Hello {self.get("name")}!</div>'
  8. });

一个Intact组件,必须包含template属性才能被实例化,关于template模板语法可以参考vdt文档。

通过Intact.mount方法,可以将该组件挂载到指定元素下。

  1. window.app = Intact.mount(App, document.getElementById('app'));

Intact.mount()方法会返回挂载组件的实例,为方便大家测试,将它赋给window.app,如果你打开控制台输入

  1. app.set('name', 'World')

可以看到界面会做相应更新。

通过上述例子,可以看出一个组件主要包括以下属性:

  1. defaults:定义组件所需要的默认数据
  2. template: 定义组件的模板

然后在模板中通过self.get('name')获取数据,在组件中通过this.set('name', 'value')改变数据,一旦set方法触发了数据变更,模板就会相应更新。

关于模板中为什么是self.get(),而不是this.get(),是因为Intact基于Vdt(虚拟DOM模板引擎)设计,详见 this & self

条件与循环

vdt模板语法非常灵活,从jsx语法衍生而来,所以可以直接在模板中书写任意的js代码。但为了方便书写和阅读,vdt提供了一些指令来实现条件和循环渲染

条件渲染 v-if

使用v-if指令可以实现一个元素的删除和添加(并非display: none)

  1. var App = Intact.extend({
  2. defaults: function() {
  3. return {show: true};
  4. },
  5. template: '<div><div v-if={self.get("show")}>展示出来</div></div>'
  6. });
  7. window.appvif = Intact.mount(App, document.getElementById('app_v_if'));

现在打开控制台,输入以下代码,让它消失

  1. appvif.set('show', false);

组件的template必须返回一个元素节点,不能为undefined,所以根节点不能删除,这也是上述例子v-if所在元素被另一个div包起来的原因

循环渲染 v-for

v-for指令可以遍历数组和对象。在循环中,被循环的元素以及子元素,都可以通过valuekey来访问遍历对象每一项的值和键。

  1. var App = Intact.extend({
  2. defaults: function() {
  3. return {
  4. list: ['Javascript', 'PHP', 'Java']
  5. };
  6. },
  7. template: '<ul>' +
  8. '<li v-for={self.get("list")} class={value}>{value}</li>' +
  9. '</ul>'
  10. });
  11. window.appvfor = Intact.mount(App, document.getElementById('appvfor'));

下面我们改变list变量,需要注意的是,数组为引用类型,如果你直接操作它,Intact并不能检测到list已经改变,所以这里需要克隆数组再操作。打开控制台,输入以下代码,更新list

  1. // concat方法会返回新数组
  2. appvfor.set('list', appvfor.get('list').concat(['C++']));

如果调用push方法直接操作数组,Intact将检测不到更新,此时可以调用appvfor.update()方法强制更新界面。

处理用户交互

事件绑定

通过ev-*指令可以在模板中绑定事件,它的值为事件处理函数,例如

  1. var App = Intact.extend({
  2. defaults: function() {
  3. return {show: true};
  4. },
  5. template: '<div>' +
  6. '<div v-if={self.get("show")}>展示出来</div>' +
  7. '<button ev-click={self.toggle}>展示/隐藏内容</button>' +
  8. '</div>',
  9. toggle: function() {
  10. this.set('show', !this.get('show'));
  11. }
  12. });
  13. Intact.mount(App, document.getElementById('appev'));

对于事件回调函数toggle,需要通过bind方法来指定this,并且还可以通过它来绑定参数,例如:bind(self, 'test')。另外可以看到,给组件添加方法很简单,直接定义即可,并不需要挂载到某个变量下。

@since v2.2.0 事件回调函数会自动bind(self),对于无需传递参数的情况,不用再次bind

ev-*指令不仅可以绑定原生浏览器事件,对于组件暴露的事件也可以直接绑定

表单操作

通过v-model指令可以方便地进行表单数据的双向绑定。

  1. var App = Intact.extend({
  2. template: '<div>' +
  3. '<input v-model="value" />' +
  4. '<div>Your input: {self.get("value")}</div>' +
  5. '</div>'
  6. });
  7. Intact.mount(App, document.getElementById('appvmodel'));

组件化编程

上面的例子中,我们都是单个组件挂载到指定元素上。在实际应用中,不可能在一个组件上完成所有的工作,而是,将界面拆分成各个小组件,通过组件间嵌套组合完成一个复杂的页面。下面我们将通过一个例子来一起学习如何使用组件化编程。

例如:实现一个TodoList,它包含以下元素,一个输入框,然后是展示每一项数据的TodoItem

  1. <TodoList>
  2. <Input />
  3. <TodoItem />
  4. </TodoList>

首先我们来定义TodoItem组件

  1. var TodoItem = Intact.extend({
  2. defaults: function() {
  3. return {
  4. todo: ''
  5. };
  6. },
  7. template: '<div>{self.get("todo")}</div>'
  8. });

该组件接受一个字符串类型的属性todo,然后将它渲染出来。我们还应该暴露一个删除事件,让TodoList来删除相应数据项。通过组件的trigger()方法,组件可以抛出任意事件。

改进后的TodoItem组件,我们在点击删除按钮时,抛出delete事件

  1. var TodoItem = Intact.extend({
  2. defaults: function() {
  3. return {
  4. todo: ''
  5. };
  6. },
  7. template: '<div>' +
  8. '{self.get("todo")}' +
  9. '<button ev-click={self.delete} type="button">X</button>' +
  10. '</div>',
  11. delete: function() {
  12. this.trigger('delete');
  13. }
  14. });

然后我们定义TotoList组件如下

  1. var TodoList = Intact.extend({
  2. defaults() {
  3. return {
  4. list: [
  5. '吃饭',
  6. '睡觉',
  7. '打豆豆'
  8. ]
  9. }
  10. },
  11. template: '<form>' +
  12. '<input />' +
  13. '<TodoItem v-for={self.get("list")} todo={value} />' +
  14. '</form>'
  15. });

我们在模板中访问了TodoItem组件,如果TodoItem是个全局变量,则模板中可以直接访问到,但大多数情况下,我们采用模块化编程方式,所有的组件都应该是局部变量,然后通过模块加载器来加载。这里想要在模板中访问到TodoItem只需要将它挂载到TodoList的实例上即可。

需要注意的是:在模板中,组件命名,首字母必须大写

另外我们还需绑定表单的submit事件,让表单提交时增加一项TodoItem。同时绑定TodoItem暴露的delete事件,执行相应的删除操作。前面我们提过,ev-*指令不仅可以绑定原生浏览器事件,还可以绑定组件自定义事件,下面我们来验证一下。

改进后的TodoList如下

  1. var TodoList = Intact.extend({
  2. defaults() {
  3. // 绑定到this上
  4. // 这样在模板中直接通过self.TodoItem即可获取
  5. this.TodoItem = TodoItem;
  6. return {
  7. list: [
  8. '吃饭',
  9. '睡觉',
  10. '打豆豆'
  11. ]
  12. }
  13. },
  14. template: 'var TodoItem = self.TodoItem;' +
  15. '<form ev-submit={self.addNewItem}>' +
  16. '<input v-model="newItem" style="margin-bottom: 10px;" />' +
  17. '<TodoItem v-for={self.get("list")} todo={value}' +
  18. 'ev-delete={self.delete.bind(self, key)}' +
  19. '/>' +
  20. '</form>',
  21. addNewItem: function(e) {
  22. e.preventDefault();
  23. if (!this.get('newItem')) return;
  24. var list = this.get('list');
  25. var newItem = this.get('newItem');
  26. this.set({
  27. 'list': list.concat([newItem]),
  28. 'newItem': '' // 清空输入
  29. });
  30. },
  31. delete(index) {
  32. // 由于数组是引用类型,克隆数组再操作
  33. var list = this.get('list').slice(0);
  34. list.splice(index, 1);
  35. this.set('list', list);
  36. }
  37. });
  1. Intact.mount(TodoList, document.getElementById('todo'));

对于初次接触MV*框架的同学来说,这个例子可能稍显复杂,不过没关系后面我们会对上面提到的点做详细说明。

组件继承

组件继承是Intact的一大特色,借助于Vdt模板引擎强大的继承机制,可以提供更灵活的组件复用能力。在了解组件继承之前,可以先了解下vdt模板继承功能

我们先来看一个简单的例子。假设存在两个页面,它们结构相同,有相同的头部和尾部,只是内容区域不同,则我们可以提取如下结构。

  1. <div>
  2. <header>header</header>
  3. <div class="content"></div>
  4. <footer>footer</footer>
  5. </div>

于是,我们可以定义一个组件Layout来描绘页面的大体结构。

  1. var Layout = Intact.extend({
  2. template: '<div>' +
  3. '<header>header {self.get("title")}</header>' +
  4. '<div class="content">' +
  5. // 此处利用vdt模板的block语法,声明一个可填充区域
  6. '<b:content />' +
  7. '</div>' +
  8. '<footer>footer</footer>' +
  9. '</div>',
  10. // 定义默认数据
  11. defaults: function() {
  12. return {title: 'Layout'};
  13. }
  14. });

下面继承Layout来实现A页面组件

  1. var A = Layout.extend({
  2. template: '<t:parent>' +
  3. '<b:content>A页面</b:content>' +
  4. '</t:parent>',
  5. defaults: function() {
  6. return {title: 'Page A'};
  7. }
  8. });

对于B页面

  1. var B = Layout.extend({
  2. template: '<t:parent>' +
  3. '<b:content>B页面</b:content>' +
  4. '</t:parent>',
  5. defaults: function() {
  6. return {title: 'Page B'};
  7. }
  8. });

最后我们模拟浏览器路由来分别展示A/B页面

  1. var App = Intact.extend({
  2. defaults: function() {
  3. return {Page: A};
  4. },
  5. template: 'var Page = self.get("Page");' +
  6. '<div>' +
  7. '<Page />' +
  8. '<button ev-click={self.toggle}>切换页面</button>' +
  9. '</div>',
  10. toggle: function() {
  11. this.set('Page', this.get('Page') === A ? B : A);
  12. }
  13. });
  14. Intact.mount(App, document.getElementById('extend'));

通过上例可以看到,对于A/B两个大致相同的页面,通过继承的方式,将公共方法和公共页面结构提取成父组件,然后各个页面仅仅实现自己的特有逻辑,可以充分复用代码。最最重要的是,子组件A/B拥有整个页面的控制权,你完全可以在A/B中随意改变页面结构,而无需父组件提供相应接口来执行相应方法。

ES6组件定义方式

使用ES6定义组件,主要有以下两点差别

  1. Intact.extend()方法对于对象字面量定义的defaults,会在组件继承时 自动将父组件与子组件定义的数据合并,所以如果ES6组件继承时需要合并父组件定义的数据, 你需要手动合并他们。
  2. ES6对于原型链上的属性,需要通过getter的方式定义

例如:

  1. class App extends Intact {
  2. defaults() {
  3. // 如果需要,可以手动合并defaults属性
  4. return {
  5. ...super.defaults(),
  6. name: 'Intact'
  7. };
  8. }
  9. // 通过getter定义template属性
  10. // 使用修饰器,让模板可以直接在派生组件中通过<t:parent>直接引用
  11. @Intact.template()
  12. get template() {
  13. return '<div>Hello {self.get("name")}!</div>'
  14. }
  15. // 或者也可以定义成类的静态属性 @since v2.2.0
  16. // @Intact.template()
  17. // static template = '<div>Hello {self.get("name")}!</div>';
  18. }

static语法需要stage-0支持npm install babel-preset-stage-0 --save-dev同时修改.babelrc,添加presets: ['stage-0']

进一步了解

看到这里,相信大家对Intact有了大致的了解,但其中还有很多细节我们没有披露。另外例子中模板template的定义也不优雅。在后面的章节中,我们将会详细讲解Intact的各个细节,以及如何使用webpack + vdt-loader来更好地组织文件。