JSX是什么?

JSX 是一种 Javascript 的语法扩展,JSX = Javascript + XML,即在 Javascript 里面写 XML,因为 JSX 的这个特性,所以他即具备了 Javascript 的灵活性,同时又兼具 html 的语义化和直观性。

注:灵活度强的部分组件可以用JSX来代替,整个工程没有必要都使用JSX。

vue工程配置JSX相关插件

我们一般使用的渲染函数时这样的:

createElement(
 'anchored-heading', {
 props: {
  level: 1
 }
 }, [
 createElement('span', 'Hello'),
 ' world!'
 ]
)

对应的模板如下:

<anchored-heading :level="1">
 <span>Hello</span> world!
</anchored-heading>

如果这样使用原生的方式来写比较麻烦,那么我可以借助babel插件。

如果是babel7.x那么需要安装如下插件

npm i @vue/babel-preset-jsx @vue/babel-helper-vue-jsx-merge-props -D

配置的babel.config.js:

module.exports = {
  presets: [
    [
      '@vue/babel-preset-jsx',
      {
        vModel: false,
        compositionAPI: true,
      },
    ],
  ],
}

如果是babel6.x那么需要安装如下插件

npm install\
  babel-plugin-syntax-jsx\
  babel-plugin-transform-vue-jsx\
  babel-helper-vue-jsx-merge-props\
  babel-preset-env\
  --save-dev

.babelrc:

{
  "presets": ["env"],
  "plugins": ["transform-vue-jsx"]
}

如果使用的vuecli2脚手架,那么默认安装了babel6.x相关的插件,不需要再重复安装。

JSX基础用法

我们新建一个HelloWorld组件,以前都是HelloWorld.vue的文件,现在我们可以使用HelloWorld.js来创建这个组件,下面以HelloWorld组件为例进行讲解。

自定义组件

我们在HelloWorld组件中也可以使用自定义的组件,HelloWorld.js代码如下:

const ButtonCounter = {
  name: "button-counter",
  props: ["count"],
  methods: {
    onClick() {
      this.$emit("changeNum", this.count + 1);
    }
  },
  render() {
    return <button onClick={this.onClick}>数量:{this.count}</button>;
  }
};

export default {
  name: "HelloWorld",
  props: {
    msg: String
  },
  data() {
    return {
      count: 0
    };
  },
  methods: {
    // 改变button按钮数量
    changeNum(val) {
      this.count = val;
    }
  },
  render() {
    const { count } = this; // 解构
    return (
      <div class="hello-world-content">
        <ButtonCounter style={{ marginTop: "20px" }} count={count} onChangeNum={this.changeNum}></ButtonCounter>
      </div>
    );
  }
};

普通属性、行内样式、动态class与style

const ButtonCounter = {
  name: "button-counter",
  props: ["count"],
  methods: {
    onClick() {
      this.$emit("changeNum", this.count + 1);
    }
  },
  render() {
    return <button onClick={this.onClick}>数量:{this.count}</button>;
  }
};

export default {
  name: "HelloWorld",
  props: {
    msg: String
  },
  data() {
    return {
      count: 0,
      text: "Hello World!",
      msgClass: "msg-class",
      isGreen: true
    };
  },
  methods: {
    // 改变button按钮数量
    changeNum(val) {
      this.count = val;
    }
  },
  render() {
    const { count, text } = this; // 解构
    return (
      <div class="hello-world-content">
        <p style="color:red">{text}</p>
        <p class={this.msg ? this.msgClass : ""}>动态绑定class,传递过来的消息:{this.msg}</p>
        <p style={this.isGreen ? "color: green" : ""}>动态绑定style,传递过来的消息:{this.msg}</p>
        <input placeholder="placeholder属性"></input>
        <ButtonCounter style={{ marginTop: "20px" }} count={count} onChangeNum={this.changeNum}></ButtonCounter>
      </div>
    );
  }
};

展示效果:

babel使用 vue yarn vue jsx babel_jsx

DOM渲染结构如下:

babel使用 vue yarn vue jsx babel_插槽_02

常用指令

v-html、v-if、v-for、v-model常用指令在JSX中无法使用,需要使用其他方式来实现。

v-html

在JSX中,如果要设置DOM的innerHTML,需要用到domProps

组件使用:

<HelloWorld msg="<div class='custom-div'>这是自定义的DOM</div>"> </HelloWorld>

组件代码:

export default {
  name: "HelloWorld",
  props: {
    msg: String
  },
  data() {
    return {};
  },
  methods: {},
  render() {
    return <div domPropsInnerHTML={this.msg}></div>;
  }
};

渲染DOM结果:

babel使用 vue yarn vue jsx babel_JSX_03

v-for

使用map来实现:

render() {
  const list = [1,2,3]
  return( 
    <div>
      { list.map(item => <button>按钮{item}</button>) }
    </div>
  )
}

v-if

简单示例:

render() {
    const bool = false;
    return <div>{bool ? <button>按钮1</button> : <button>按钮2</button>}</div>;
  }

复杂示例:

render() {
  let num = 3
  if(num === 1){ return( <button>按钮1</button> ) }
  if(num === 2){ return( <button>按钮2</button> ) }
  if(num === 3){ return( <button>按钮3</button> ) }
}

v-model

注:新版 vue-cli4 中,已经默认集成了 JSX 语法对 v-model 的支持,可以直接使用
<input v-model={this.value}>

如果是老项目比如脚手架vuecli2,那么需要安装插件babel-plugin-jsx-v-model,同时在.babelrc文件中加入如下插件配置:

"plugins": ["jsx-v-model"]

使用示例:

export default {
  name: "HelloWorld",
  props: {
    msg: String
  },
  data() {
    return {
      value: "abc"
    };
  },
  watch: {
    value(val) {
      console.log("this.model内容:" + val);
    }
  },
  methods: {},
  render() {
    return (
      <div>
        <input v-model={this.value} placeholder="普通文本" />
      </div>
    );
  }
};

监听事件、事件修饰符

监听事件

监听事件想到用 onChange, onClick等。
需要注意的是,传参数不能使用 onClick={this.handleClick(params)},这样子会每次 render 的时候都会自动执行一次方法。

应该使用 bind,或者箭头函数来传参。

组件示例代码:

export default {
  name: "HelloWorld",
  props: {
    msg: String
  },
  data() {
    return {};
  },
  methods: {
    handleClick(val) {
      alert(val);
    }
  },
  render() {
    return (
      <div>
        <button type="button" onClick={this.handleClick.bind(this, 11)}>
          方式一
        </button>
        <button type="button" onClick={() => this.handleClick(22)}>
          方式二
        </button>
      </div>
    );
  }
};

用监听事件来实现v-model:

methods: {
    input(e) {
      this.value = e.target.value;
    }
  },
  render() {
    return (
      <div>
        <input type="text" value={this.value} onInput={this.input} />
      </div>
    );
  }

也可以调整为:

<input type="text" value={this.value} onInput={(e) => (this.vaue = e.target.value)} />

还可以使用对象的方式去监听事件:

export default {
  name: "HelloWorld",
  props: {
    msg: String
  },
  data() {
    return {
      value: ""
    };
  },
  watch: {
    value(val) {
      console.log("this.model的内容:" + val);
    }
  },
  methods: {
    handleInput(e) {
      this.value = e.target.value;
    },
    handleFocus(e) {
      console.log(e.target);
    }
  },
  render() {
    return (
      <div>
        <input type="text" value={this.value} {...{ on: { input: this.handleInput, focus: this.handleFocus } }} />
      </div>
    );
  }
};

nativeOn仅对于组件,用于监听原生事件,也可以使用对象的方式去监听事件:

{...{nativeOn:{click: this.handleClick}}}

事件修饰符

和指令一样,除了个别的之外,大部分的事件修饰符都无法在JSX中使用。

  • .stop : 阻止事件冒泡,在JSX中使用event.stopPropagation()来代替
  • .prevent:阻止默认行为,在JSX中使用event.preventDefault() 来代替
  • .self:只当事件是从侦听器绑定的元素本身触发时才触发回调,使用下面的条件判断进行代替
if (event.target !== event.currentTarget){
  return
}

.enter与keyCode: 在特定键触发时才触发回调

if(event.keyCode === 13) {
  // 执行逻辑
}

除了上面这些修饰符之外,尤大大对于.once,.capture,.passive,.capture.once做了优化,简化代码:

export default {
  name: "HelloWorld",
  props: {
    msg: String
  },
  methods: {
    handleClick(e) {
      console.log("click事件:" + e.target);
    },
    handleInput(e) {
      console.log("input事件:" + e.target);
    },
    handleMouseDown(e) {
      console.log("mousedown事件:" + e.target);
    },
    handleMouseUp(e) {
      console.log("mouseup事件" + e.target);
    }
  },
  render() {
    return (
      <div
        {...{
          on: {
            // 相当于 :click.capture
            "!click": this.handleClick,
            // 相当于 :input.once
            "~input": this.handleInput,
            // 相当于 :mousedown.passive
            "&mousedown": this.handleMouseDown,
            // 相当于 :mouseup.capture.once
            "~!mouseup": this.handleMouseUp
          }
        }}
      >
        点击模块
      </div>
    );
  }
};

结合第三方UI库组件

不仅仅在 render 函数里面使用 JSX,而且还可以在 methods 里面返回 JSX,然后在 render 函数里面调用这个方法。并且也可以直接使用例如elementui等ui组件。

示例:

export default {
  name: "HelloWorld",
  props: {
    msg: String
  },
  data() {
    return {
      visible: false
    };
  },
  methods: {
    renderFooter() {
      return (
        <div>
          <el-button onClick={() => (this.visible = false)}>确定</el-button>
          <el-button onClick={() => this.closeDialog()}>取消</el-button>
        </div>
      );
    },
    openDialog() {
      this.visible = true;
    },
    closeDialog() {
      this.visible = false;
    }
  },
  render() {
    const footerHTML = this.renderFooter();
    return (
      <div>
        <button onClick={() => this.openDialog()}>打开弹框</button>
        <el-dialog visible={this.visible} title="弹框提示" width="200px" show-close={true} before-close={() => (this.visible = false)}>
          <div>弹框内容</div>
          <template slot="footer">{footerHTML}</template>
        </el-dialog>
      </div>
    );
  }
};

效果图:

babel使用 vue yarn vue jsx babel_JSX_04

插槽

普通插槽与具名插槽

示例:

<HelloWorld>
    <template slot="default">默认内容</template>
    <template slot="footer">
      <el-button type="primary">确定</el-button>
      <el-button>取消</el-button>
    </template>
  </HelloWorld>

HelloWorld组件代码:

export default {
  name: "HelloWorld",
  render() {
    return (
      <div>
        <div class="default">{this.$slots.default}</div>
        <div class="footer">{this.$slots.footer}</div>
      </div>
    );
  }
};

作用域插槽

示例:

<HelloWorld>
    <template v-slot:content="{ name, age }">
      <div>姓名:{{ name }}</div>
      <div>年龄:{{ age }}</div>
    </template>
  </HelloWorld>

HelloWorld组件代码:

export default {
  name: "HelloWorld",
  render() {
    return (
      <div>
        <div class="content">{this.$scopedSlots.content({ name: "张三", age: 20 })}</div>
      </div>
    );
  }
};

子组件通过{this.$scopedSlots.content({ name: "张三", age: 20 })}指定插槽的名称为content,并将含有name,age属性的对象数据传递给父组件,父组件就可以在插槽内容中使用子组件传递来的数据。