zhouciming

zhouciming 查看完整档案

填写现居城市  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 该用户太懒什么也没留下

个人动态

zhouciming 赞了文章 · 2019-11-25

React入门看这篇就够了

react - JSX

React 背景介绍

React 起源于 Facebook 的内部项目,因为该公司对市场上所有 JavaScript MVC 框架,都不满意,就决定自己写一套,用来架设 Instagram 的网站。做出来以后,发现这套东西很好用,就在2013年5月开源了。

什么是React

  • A JAVASCRIPT LIBRARY FOR BUILDING USER INTERFACES

    • 用来构建UI的 JavaScript库
    • React 不是一个 MVC 框架,仅仅是视图(V)层的库
  • React 官网
  • React 中文文档

特点

  • 1 使用 JSX语法 创建组件,实现组件化开发,为函数式的 UI 编程方式打开了大门
  • 2 性能高的让人称赞:通过 diff算法虚拟DOM 实现视图的高效更新
  • 3 HTML仅仅是个开始
> JSX --TO--> EveryThing

- JSX --> HTML
- JSX --> native ios或android中的组件(XML)
- JSX --> VR
- JSX --> 物联网

为什么要用React

  • 1 使用组件化开发方式,符合现代Web开发的趋势
  • 2 技术成熟,社区完善,配件齐全,适用于大型Web项目(生态系统健全)
  • 3 由Facebook专门的团队维护,技术支持可靠
  • 4 ReactNative - Learn once, write anywhere: Build mobile apps with React
  • 5 使用方式简单,性能非常高,支持服务端渲染
  • 6 React非常火,从技术角度,可以满足好奇心,提高技术水平;从职业角度,有利于求职和晋升,有利于参与潜力大的项目

React中的核心概念

  • 1 虚拟DOM(Virtual DOM)
  • 2 Diff算法(虚拟DOM的加速器,提升React性能的法宝)

虚拟DOM(Vitural DOM)

React将DOM抽象为虚拟DOM,虚拟DOM其实就是用一个对象来描述DOM,通过对比前后两个对象的差异,最终只把变化的部分重新渲染,提高渲染的效率

为什么用虚拟dom,当dom反生更改时需要遍历 而原生dom可遍历属性多大231个 且大部分与渲染无关 更新页面代价太大

VituralDOM的处理方式

  • 1 用 JavaScript 对象结构表示 DOM 树的结构,然后用这个树构建一个真正的 DOM 树,插到文档当中
  • 2 当状态变更的时候,重新构造一棵新的对象树。然后用新的树和旧的树进行比较,记录两棵树差异
  • 3 把2所记录的差异应用到步骤1所构建的真正的DOM树上,视图就更新了

Diff算法

当你使用React的时候,在某个时间点 render() 函数创建了一棵React元素树,
在下一个state或者props更新的时候,render() 函数将创建一棵新的React元素树,
React将对比这两棵树的不同之处,计算出如何高效的更新UI(只更新变化的地方)
<!-- 了解:

有一些解决将一棵树转换为另一棵树的最小操作数算法问题的通用方案。然而,树中元素个数为n,最先进的算法 的时间复杂度为O(n3) 。
如果直接使用这个算法,在React中展示1000个元素则需要进行10亿次的比较。这操作太过昂贵,相反,React基于两点假设,实现了一个O(n)算法,提升性能: -->
  • React中有两种假定:

    • 1 两个不同类型的元素会产生不同的树(根元素不同结构树一定不同)
    • 2 开发者可以通过key属性指定不同树中没有发生改变的子元素

Diff算法的说明 - 1

  • 如果两棵树的根元素类型不同,React会销毁旧树,创建新树
// 旧树
<div>
  <Counter />
</div>

// 新树
<span>
  <Counter />
</span>

执行过程:destory Counter -> insert Counter

Diff算法的说明 - 2

  • 对于类型相同的React DOM 元素,React会对比两者的属性是否相同,只更新不同的属性
  • 当处理完这个DOM节点,React就会递归处理子节点。
// 旧
<div className="before" title="stuff" />
// 新
<div className="after" title="stuff" />
只更新:className 属性

// 旧
<div style={{color: 'red', fontWeight: 'bold'}} />
// 新
<div style={{color: 'green', fontWeight: 'bold'}} />
只更新:color属性

Diff算法的说明 - 3

  • 1 当在子节点的后面添加一个节点,这时候两棵树的转化工作执行的很好
// 旧
<ul>
  <li>first</li>
  <li>second</li>
</ul>

// 新
<ul>
  <li>first</li>
  <li>second</li>
  <li>third</li>
</ul>

执行过程:
React会匹配新旧两个<li>first</li>,匹配两个<li>second</li>,然后添加 <li>third</li> tree
  • 2 但是如果你在开始位置插入一个元素,那么问题就来了:
// 旧
<ul>
  <li>Duke</li>
  <li>Villanova</li>
</ul>

// 新
<ul>
  <li>Connecticut</li>
  <li>Duke</li>
  <li>Villanova</li>
</ul>

在没有key属性时执行过程:
React将改变每一个子删除重新创建,而非保持 <li>Duke</li> 和 <li>Villanova</li> 不变

key 属性

为了解决以上问题,React提供了一个 key 属性。当子节点带有key属性,React会通过key来匹配原始树和后来的树。
// 旧
<ul>
  <li key="2015">Duke</li>
  <li key="2016">Villanova</li>
</ul>

// 新
<ul>
  <li key="2014">Connecticut</li>
  <li key="2015">Duke</li>
  <li key="2016">Villanova</li>
</ul>
执行过程:
现在 React 知道带有key '2014' 的元素是新的,对于 '2015' 和 '2016' 仅仅移动位置即可 
  • 说明:key属性在React内部使用,但不会传递给你的组件
  • 推荐:在遍历数据时,推荐在组件中使用 key 属性:<li key={item.id}>{item.name}</li>
  • 注意:key只需要保持与他的兄弟节点唯一即可,不需要全局唯一
  • 注意:尽可能的减少数组index作为key,数组中插入元素的等操作时,会使得效率底下

React的基本使用

  • 安装:npm i -S react react-dom
  • react:react 是React库的入口点
  • react-dom:提供了针对DOM的方法,比如:把创建的虚拟DOM,渲染到页面上
// 1. 导入 react
import React from 'react'
import ReactDOM from 'react-dom'

// 2. 创建 虚拟DOM
// 参数1:元素名称  参数2:元素属性对象(null表示无)  参数3:当前元素的子元素string||createElement() 的返回值
const divVD = React.createElement('div', {
  title: 'hello react'
}, 'Hello React!!!')

// 3. 渲染
// 参数1:虚拟dom对象  参数2:dom对象表示渲染到哪个元素内 参数3:回调函数
ReactDOM.render(divVD, document.getElementById('app'))

createElement()的问题

  • 说明:createElement()方式,代码编写不友好,太复杂
var dv = React.createElement(
  "div",
  { className: "shopping-list" },
  React.createElement(
    "h1",
    null,
    "Shopping List for "
  ),
  React.createElement(
    "ul",
    null,
    React.createElement(
      "li",
      null,
      "Instagram"
    ),
    React.createElement(
      "li",
      null,
      "WhatsApp"
    )
  )
)
// 渲染
ReactDOM.render(dv, document.getElementById('app'))

JSX 的基本使用

  • 注意:JSX语法,最终会被编译为 createElement() 方法
  • 推荐:使用 JSX 的方式创建组件
  • JSX - JavaScript XML
  • 安装:npm i -D babel-preset-react (依赖与:babel-core/babel-loader)
注意:JSX的语法需要通过 babel-preset-react 编译后,才能被解析执行
/* 1 在 .babelrc 开启babel对 JSX 的转换 */
{
  "presets": [
    "env", "react"
  ]
}

/* 2 webpack.config.js */
module: [
  rules: [
    { test: /\.js$/, use: 'babel-loader', exclude: /node_modules/ },
  ]
]

/* 3 在 js 文件中 使用 JSX */
const dv = (
  <div title="标题" className="cls container">Hello JSX!</div>
)

/* 4 渲染 JSX 到页面中 */
ReactDOM.render(dv, document.getElementById('app'))

JSX的注意点

  • 注意 1: 如果在 JSX 中给元素添加类, 需要使用 className 代替 class

    • 类似:label 的 for属性,使用htmlFor代替
  • 注意 2:在 JSX 中可以直接使用 JS代码,直接在 JSX 中通过 {} 中间写 JS代码即可
  • 注意 3:在 JSX 中只能使用表达式,但是不能出现 语句!!!
  • 注意 4:在 JSX 中注释语法:{/* 中间是注释的内容 */}

React组件

React 组件可以让你把UI分割为独立、可复用的片段,并将每一片段视为相互独立的部分。
  • 组件是由一个个的HTML元素组成的
  • 概念上来讲, 组件就像JS中的函数。它们接受用户输入(props),并且返回一个React对象,用来描述展示在页面中的内容

React创建组件的两种方式

  • 1 通过 JS函数 创建(无状态组件)
  • 2 通过 class 创建(有状态组件)
函数式组件 和 class 组件的使用场景说明:
1 如果一个组件仅仅是为了展示数据,那么此时就可以使用 函数组件
2 如果一个组件中有一定业务逻辑,需要操作数据,那么就需要使用 class 创建组件,因为,此时需要使用 state

JavaScript函数创建

  • 注意:1 函数名称必须为大写字母开头,React通过这个特点来判断是不是一个组件
  • 注意:2 函数必须有返回值,返回值可以是:JSX对象或null
  • 注意:3 返回的JSX,必须有一个根元素
  • 注意:4 组件的返回值使用()包裹,避免换行问题
function Welcome(props) {
  return (
    // 此处注释的写法 
    <div className="shopping-list">
      {/* 此处 注释的写法 必须要{}包裹 */}
      <h1>Shopping List for {props.name}</h1>
      <ul>
        <li>Instagram</li>
        <li>WhatsApp</li>
      </ul>
    </div>
  )
}

ReactDOM.render(
  <Welcome name="jack" />,
  document.getElementById('app')
)

class创建

在es6中class仅仅是一个语法糖,不是真正的类,本质上还是构造函数+原型 实现继承
// ES6中class关键字的简单使用

// - **ES6中的所有的代码都是运行在严格模式中的**
// - 1 它是用来定义类的,是ES6中实现面向对象编程的新方式
// - 2 使用`static`关键字定义静态属性
// - 3 使用`constructor`构造函数,创建实例属性
// - [参考](http://es6.ruanyifeng.com/#docs/class)

// 语法:
class Person {
  // 实例的构造函数 constructor
  constructor(age){
    // 实例属性
    this.age = age
  }
  // 在class中定义方法 此处为实例方法 通过实例打点调用
  sayHello () {
    console.log('大家好,我今年' + this.age + '了');
  }

  // 静态方法 通过构造函数打点调用 Person.doudou()
  static doudou () {
    console.log('我是小明,我新get了一个技能,会暖床');
  }
}
// 添加静态属性
Person.staticName = '静态属性'
// 实例化对象
const p = new Person(19)
 
 
// 实现继承的方式
 
class American extends Person {
  constructor() {
    // 必须调用super(), super表示父类的构造函数
    super()
    this.skin = 'white'
    this.eyeColor = 'white'
  }
}

// 创建react对象
// 注意:基于 `ES6` 中的class,需要配合 `babel` 将代码转化为浏览器识别的ES5语法
// 安装:`npm i -D babel-preset-env`
 
//  react对象继承字React.Component
class ShoppingList extends React.Component {
  constructor(props) { 
    super(props)
  }
  // class创建的组件中 必须有rander方法 且显示return一个react对象或者null
  render() {
    return (
      <div className="shopping-list">
        <h1>Shopping List for {this.props.name}</h1>
        <ul>
          <li>Instagram</li>
          <li>WhatsApp</li>
        </ul>
      </div>
    )
  }
}

给组件传递数据 - 父子组件传递数据

  • 组件中有一个 只读的对象 叫做 props,无法给props添加属性
  • 获取方式:函数参数 props
  • 作用:将传递给组件的属性转化为 props 对象中的属性
function Welcome(props){
  // props ---> { username: 'zs', age: 20 }
  return (
    <div>
      <div>Welcome React</div>
      <h3>姓名:{props.username}----年龄是:{props.age}</h3>
    </div>
  )
}

// 给 Hello组件 传递 props:username 和 age(如果你想要传递numb类型是数据 就需要向下面这样)
ReactDOM.reander(<Hello username="zs" age={20}></Hello>, ......)

封装组件到独立的文件中

// 创建Hello2.js组件文件
// 1. 引入React模块
// 由于 JSX 编译后会调用 React.createElement 方法,所以在你的 JSX 代码中必须首先拿到React。
import React from 'react'

// 2. 使用function构造函数创建组件
function Hello2(props){
  return (
    <div>
      <div>这是Hello2组件</div>
      <h1>这是大大的H1标签,我大,我骄傲!!!</h1>
      <h6>这是小小的h6标签,我小,我傲娇!!!</h6>
    </div>
  )
}
// 3. 导出组件
export default Hello2

// app.js中   使用组件:
import Hello2 from './components/Hello2'

props和state

props

  • 作用:给组件传递数据,一般用在父子组件之间
  • 说明:React把传递给组件的属性转化为一个对象并交给 props
  • 特点:props是只读的,无法给props添加或修改属性
  • props.children:获取组件的内容,比如:

    • <Hello>组件内容</Hello> 中的 组件内容
// props 是一个包含数据的对象参数,不要试图修改 props 参数
// 返回值:react元素
function Welcome(props) {
  // 返回的 react元素中必须只有一个根元素
  return <div>hello, {props.name}</div>
}

class Welcome extends React.Component {
  constructor(props) {
    super(props)
  }

  render() {
    return <h1>Hello, {this.props.name}</h1>
  }
}

state

状态即数据
  • 作用:用来给组件提供组件内部使用的数据
  • 注意:只有通过class创建的组件才具有状态
  • 注意:状态是私有的,完全由组件来控制
  • 注意:不要在 state 中添加 render() 方法中不需要的数据,会影响渲染性能!

    • 可以将组件内部使用但是不渲染在视图中的内容,直接添加给 this
  • 注意:不要在 render() 方法中调用 setState() 方法来修改state的值

    • 但是可以通过 this.state.name = 'rose' 方式设置state(不推荐!!!!)
// 例:
class Hello extends React.Component {
  constructor() {
    // es6继承必须用super调用父类的constructor
    super()

    this.state = {
      gender: 'male'
    }
  }

  render() {
    return (
      <div>性别:{ this.state.gender }</div>
    )
  }
}

JSX语法转化过程

// 1、JSX
const element = (
  <h1 className="greeting">
    Hello, world!
  </h1>
)

// 2、JSX -> createElement
const element = React.createElement(
  'h1',
  {className: 'greeting'},
  'Hello, world!'
)

// React elements: 使用对象的形式描述页面结构
// Note: 这是简化后的对象结构
const element = {
  type: 'h1',
  props: {
    className: 'greeting',
  },
  children: ['Hello, world']
}

评论列表案例

  • 巩固有状态组件和无状态组件的使用
  • 两个组件:<CommentList></CommentList><Comment></Comment>
[
  { user: '张三', content: '哈哈,沙发' },
  { user: '张三2', content: '哈哈,板凳' },
  { user: '张三3', content: '哈哈,凉席' },
  { user: '张三4', content: '哈哈,砖头' },
  { user: '张三5', content: '哈哈,楼下山炮' }
]

// 属性扩展
<Comment {...item} key={i}></Comment>

style样式

// 1. 直接写行内样式:
<li style={{border:'1px solid red', fontSize:'12px'}}></li>

// 2. 抽离为对象形式
var styleH3 = {color:'blue'}
var styleObj = {
  liStyle:{border:'1px solid red', fontSize:'12px'},
  h3Style:{color:'green'}
}

<li style={styleObj.liStyle}>
  <h3 style={styleObj.h3Style}>评论内容:{props.content}</h3>
</li>

// 3. 使用样式表定义样式:
import '../css/comment.css'
<p className="pUser">评论人:{props.user}</p>

相关文章

组件的生命周期

  • 简单说:一个组件从开始到最后消亡所经历的各种状态,就是一个组件的生命周期

组件生命周期函数的定义:从组件被创建,到组件挂载到页面上运行,再到页面关闭组件被卸载,这三个阶段总是伴随着组件各种各样的事件,那么这些事件,统称为组件的生命周期函数!

组件生命周期函数总览

  • 组件的生命周期包含三个阶段:创建阶段(Mounting)、运行和交互阶段(Updating)、卸载阶段(Unmounting)
  • Mounting:
constructor()
componentWillMount()
render()
componentDidMount()
  • Updating
componentWillReceiveProps()
shouldComponentUpdate()
componentWillUpdate()
render()
componentDidUpdate()
  • Unmounting
componentWillUnmount()

组件生命周期 - 创建阶段(Mounting)

  • 特点:该阶段的函数只执行一次

constructor()

  • 作用:1 获取props 2 初始化state
  • 说明:通过 constructor() 的参数props获取
  • 设置state和props
class Greeting extends React.Component {
  constructor(props) {
    // 获取 props
    super(props)
    // 初始化 state
    this.state = {
      count: props.initCount
    }
  }
}

// 初始化 props
// 语法:通过静态属性 defaultProps 来初始化props
Greeting.defaultProps = {
  initCount: 0
};

componentWillMount()

  • 说明:组件被挂载到页面之前调用,其在render()之前被调用,因此在这方法里同步地设置状态将不会触发重渲染
  • 注意:无法获取页面中的DOM对象
  • 注意:可以调用 setState() 方法来改变状态值
  • 用途:发送ajax请求获取数据
componentWillMount() {
  console.warn(document.getElementById('btn')) // null
  this.setState({
    count: this.state.count + 1
  })
}

render()

  • 作用:渲染组件到页面中,无法获取页面中的DOM对象
  • 注意:不要在render方法中调用 setState() 方法,否则会递归渲染

    • 原因说明:状态改变会重新调用render()render()又重新改变状态
render() {
  console.warn(document.getElementById('btn')) // null

  return (
    <div>
      <button id="btn" onClick={this.handleAdd}>打豆豆一次</button>
      {
        this.state.count === 4
        ? null
        : <CounterChild initCount={this.state.count}></CounterChild>
      }
    </div>
  )
}

componentDidMount()

  • 1 组件已经挂载到页面中
  • 2 可以进行DOM操作,比如:获取到组件内部的DOM对象
  • 3 可以发送请求获取数据
  • 4 可以通过 setState() 修改状态的值
  • 注意:在这里修改状态会重新渲染
componentDidMount() {
  // 此时,就可以获取到组件内部的DOM对象
  console.warn('componentDidMount', document.getElementById('btn'))
}

组件生命周期 - 运行阶段(Updating)

  • 特点:该阶段的函数执行多次
  • 说明:每当组件的props或者state改变的时候,都会触发运行阶段的函数

componentWillReceiveProps()

  • 说明:组件接受到新的props前触发这个方法
  • 参数:当前组件props
  • 可以通过 this.props 获取到上一次的值
  • 使用:若你需要响应属性的改变,可以通过对比this.propsnextProps并在该方法中使用this.setState()处理状态改变
  • 注意:修改state不会触发该方法
componentWillReceiveProps(nextProps) {
  console.warn('componentWillReceiveProps', nextProps)
}

shouldComponentUpdate()

  • 作用:根据这个方法的返回值决定是否重新渲染组件,返回true重新渲染,否则不渲染
  • 优势:通过某个条件渲染组件,降低组件渲染频率,提升组件性能
  • 说明:如果返回值为false,那么,后续render()方法不会被调用
  • 注意:这个方法必须返回布尔值!!!
  • 场景:根据随机数决定是否渲染组件
// - 参数:
//   - 第一个参数:最新属性对象
//   - 第二个参数:最新状态对象
shouldComponentUpdate(nextProps, nextState) {
  console.warn('shouldComponentUpdate', nextProps, nextState)

  return nextState.count % 2 === 0
}

componentWillUpdate()

  • 作用:组件将要更新
  • 参数:最新的属性和状态对象
  • 注意:不能修改状态 否则会循环渲染
componentWillUpdate(nextProps, nextState) {
  console.warn('componentWillUpdate', nextProps, nextState)
}

render() 渲染

  • 作用:重新渲染组件,与Mounting阶段的render是同一个函数
  • 注意:这个函数能够执行多次,只要组件的属性或状态改变了,这个方法就会重新执行

componentDidUpdate()

  • 作用:组件已经被更新
  • 参数:旧的属性和状态对象
componentDidUpdate(prevProps, prevState) {
  console.warn('componentDidUpdate', prevProps, prevState)
}

组件生命周期 - 卸载阶段(Unmounting)

  • 组件销毁阶段:组件卸载期间,函数比较单一,只有一个函数,这个函数也有一个显著的特点:组件一辈子只能执行依次!
  • 使用说明:只要组件不再被渲染到页面中,那么这个方法就会被调用( 渲染到页面中 -> 不再渲染到页面中 )

componentWillUnmount()

  • 作用:在卸载组件的时候,执行清理工作,比如

    • 1 清除定时器
    • 2 清除componentDidMount创建的DOM对象

React - createClass(不推荐)

  • React.createClass({}) 方式,创建有状态组件,该方式已经被废弃!!!
  • 通过导入 require('create-react-class'),可以在不适用ES6的情况下,创建有状态组件
  • getDefaultProps() 和 getInitialState() 方法:是 createReactClass() 方式创建组件中的两个函数
  • React without ES6
  • React 不适用ES6
var createReactClass = require('create-react-class');
var Greeting = createReactClass({
  // 初始化 props
  getDefaultProps: function() {
    console.log('getDefaultProps');
    return {
      title: 'Basic counter!!!'
    }
  },

  // 初始化 state
  getInitialState: function() {
    console.log('getInitialState');
    return {
      count: 0
    }
  },

  render: function() {
    console.log('render');
    return (
      <div>
        <h1>{this.props.title}</h1>
        <div>{this.state.count}</div>
        <input type='button' value='+' onClick={this.handleIncrement} />
      </div>
    );
  },

  handleIncrement: function() {
    var newCount = this.state.count + 1;
    this.setState({count: newCount});
  },

  propTypes: {
    title: React.PropTypes.string
  }
});

ReactDOM.render(
  React.createElement(Greeting),
  document.getElementById('app')
);

state和setState

  • 注意:使用 setState() 方法修改状态,状态改变后,React会重新渲染组件
  • 注意:不要直接修改state属性的值,这样不会重新渲染组件!!!
  • 使用:1 初始化state 2 setState修改state
// 修改state(不推荐使用)
// https://facebook.github.io/react/docs/state-and-lifecycle.html#do-not-modify-state-directly
this.state.test = '这样方式,不会重新渲染组件';
constructor(props) {
  super(props)

  // 正确姿势!!!
  // -------------- 初始化 state --------------
  this.state = {
    count: props.initCount
  }
}

componentWillMount() {
  // -------------- 修改 state 的值 --------------
  // 方式一:
  this.setState({
    count: this.state.count + 1
  })

  this.setState({
    count: this.state.count + 1
  }, function(){
    // 由于 setState() 是异步操作,所以,如果想立即获取修改后的state
    // 需要在回调函数中获取
    // https://doc.react-china.org/docs/react-component.html#setstate
  });

  // 方式二:
  this.setState(function(prevState, props) {
    return {
      counter: prevState.counter + props.increment
    }
  })

  // 或者 - 注意: => 后面需要带有小括号,因为返回的是一个对象
  this.setState((prevState, props) => ({
    counter: prevState.counter + props.increment
  }))
}

组件绑定事件

  • 1 通过React事件机制 onClick 绑定
  • 2 JS原生方式绑定(通过 ref 获取元素)

    • 注意:ref 是React提供的一个特殊属性
    • ref的使用说明:react ref

React中的事件机制 - 推荐

  • 注意:事件名称采用驼峰命名法
  • 例如:onClick 用来绑定单击事件
<input type="button" value="触发单击事件"
  onClick={this.handleCountAdd}
  onMouseEnter={this.handleMouseEnter}
/>

JS原生方式 - 知道即可

  • 说明:给元素添加 ref 属性,然后,获取元素绑定事件
// JSX
// 将当前DOM的引用赋值给 this.txtInput 属性
<input ref={ input => this.txtInput = input } type="button" value="我是豆豆" />

componentDidMount() {
  // 通过 this.txtInput 属性获取元素绑定事件
  this.txtInput.addEventListener(() => {
    this.setState({
      count:this.state.count + 1
    })
  })
}

事件绑定中的this

  • 1 通过 bind 绑定
  • 2 通过 箭头函数 绑定

通过bind绑定

  • 原理:bind能够调用函数,改变函数内部this的指向,并返回一个新函数
  • 说明:bind第一个参数为返回函数中this的指向,后面的参数为传给返回函数的参数
// 自定义方法:
handleBtnClick(arg1, arg2) {
  this.setState({
    msg: '点击事件修改state的值' + arg1 + arg2
  })
}

render() {
  return (
    <div>
      <button onClick={
        // 无参数
        // this.handleBtnClick.bind(this)

        // 有参数
        this.handleBtnClick.bind(this, 'abc', [1, 2])
      }>事件中this的处理</button>
      <h1>{this.state.msg}</h1>
    </div>
  )
}
  • 在构造函数中使用bind
constructor() {
  super()

  this.handleBtnClick = this.handleBtnClick.bind(this)
}

// render() 方法中:
<button onClick={ this.handleBtnClick }>事件中this的处理</button>

通过箭头函数绑定

  • 原理:箭头函数中的this由所处的环境决定,自身不绑定this
<input type="button" value="在构造函数中绑定this并传参" onClick={
  () => { this.handleBtnClick('参数1', '参数2') }
} />

handleBtnClick(arg1, arg2) {
  this.setState({
    msg: '在构造函数中绑定this并传参' + arg1 + arg2
  });
}

受控组件

在HTML当中,像input,textareaselect这类表单元素会维持自身状态,并根据用户输入进行更新。
在React中,可变的状态通常保存在组件的state中,并且只能用 setState() 方法进行更新.
React根据初始状态渲染表单组件,接受用户后续输入,改变表单组件内部的状态。
因此,将那些值由React控制的表单元素称为:受控组件。
  • 受控组件的特点:

    • 1 表单元素
    • 2 由React通过JSX渲染出来
    • 3 由React控制值的改变,也就是说想要改变元素的值,只能通过React提供的方法来修改
  • 注意:只能通过setState来设置受控组件的值
// 模拟实现文本框数据的双向绑定
<input type="text" value={this.state.msg} onChange={this.handleTextChange}/>

// 当文本框内容改变的时候,触发这个事件,重新给state赋值
handleTextChange = event => {
  console.log(event.target.value)

  this.setState({
    msg: event.target.value
  })
}

评论列表案例

[
  {name: '小明', content: '沙发!!!'},
  {name: '小红', content: '小明,居然是你'},
  {name: '小刚', content: '小明,放学你别走!!!'},
]

props校验

  • 作用:通过类型检查,提高程序的稳定性
  • 命令:npm i -S prop-types
  • 类型校验文档
  • 使用:给类提供一个静态属性 propTypes(对象),来约束props
// 引入模块
import PropTypes from 'prop-types'

// ...以下代码是类的静态属性:
// propTypes 静态属性的名称是固定的!!!
static propTypes = {
  initCount: PropTypes.number, // 规定属性的类型
  initAge: PropTypes.number.isRequired // 规定属性的类型,且规定为必传字段
}

React 单向数据流

  • React 中采用单项数据流
  • 数据流动方向:自上而下,也就是只能由父组件传递到子组件
  • 数据都是由父组件提供的,子组件想要使用数据,都是从父组件中获取的
  • 如果多个组件都要使用某个数据,最好将这部分共享的状态提升至他们最近的父组件当中进行管理
  • 单向数据流
  • 状态提升
react中的单向数据流动:
1 数据应该是从上往下流动的,也就是由父组件将数据传递给子组件
2 数据应该是由父组件提供,子组件要使用数据的时候,直接从子组件中获取

在我们的评论列表案例中:数据是由CommentList组件(父组件)提供的
子组件 CommentItem 负责渲染评论列表,数据是由 父组件提供的
子组件 CommentForm 负责获取用户输入的评论内容,最终也是把用户名和评论内容传递给了父组件,由父组件负责处理这些数据( 把数据交给 CommentItem 由这个组件负责渲染 )

组件通讯

  • 父 -> 子:props
  • 子 -> 父:父组件通过props传递回调函数给子组件,子组件调用函数将数据作为参数传递给父组件
  • 兄弟组件:因为React是单向数据流,因此需要借助父组件进行传递,通过父组件回调函数改变兄弟组件的props
  • React中的状态管理: flux(提出状态管理的思想) -> Redux -> mobx
  • Vue中的状态管理: Vuex
  • 简单来说,就是统一管理了项目中所有的数据,让数据变的可控
  • 组件通讯

Context特性

  • 注意:如果不熟悉React中的数据流,不推荐使用这个属性

    • 这是一个实验性的API,在未来的React版本中可能会被更改
  • 作用:跨级传递数据(爷爷给孙子传递数据),避免向下每层手动地传递props
  • 说明:需要配合PropTypes类型限制来使用
class Grandfather extends React.Component {
  // 类型限制(必须),静态属性名称固定
  static childContextTypes = {
    color: PropTypes.string.isRequired
  }

  // 传递给孙子组件的数据
  getChildContext() {
    return {
      color: 'red'
    }
  }

  render() {
    return (
      <Father></Father>
    )
  }
}

class Child extends React.Component {
  // 类型限制,静态属性名字固定
  static contextTypes = {
    color: PropTypes.string
  }

  render() {
    return (
      // 从上下文对象中获取爷爷组件传递过来的数据
      <h1 style={{ color: this.context.color }}>爷爷告诉文字是红色的</h1>
    )
  }
}

class Father extends React.Component {
  render() {
    return (
      <Child></Child>
    )
  }
}

react-router

基本概念说明

  • Router组件本身只是一个容器,真正的路由要通过Route组件定义

使用步骤

  • 1 导入路由组件
  • 2 使用 <Router></Router> 作为根容器,包裹整个应用(JSX)

    • 在整个应用程序中,只需要使用一次
  • 3 使用 <Link to="/movie"></Link> 作为链接地址,并指定to属性
  • 4 使用 <Route path="/" compoent={Movie}></Route> 展示路由内容
// 1 导入组件
import {
  HashRouter as Router,
  Link, Route
} from 'react-router-dom'

// 2 使用 <Router>
<Router>

    // 3 设置 Link
    <Menu.Item key="1"><Link to="/">首页</Link></Menu.Item>
    <Menu.Item key="2"><Link to="/movie">电影</Link></Menu.Item>
    <Menu.Item key="3"><Link to="/about">关于</Link></Menu.Item>

    // 4 设置 Route
    // exact 表示:绝对匹配(完全匹配,只匹配:/)
    <Route exact path="/" component={HomeContainer}></Route>
    <Route path="/movie" component={MovieContainer}></Route>
    <Route path="/about" component={AboutContainer}></Route>

</Router>

注意点

  • <Router></Router>:作为整个组件的根元素,是路由容器,只能有一个唯一的子元素
  • <Link></Link>:类似于vue中的<router-link></router-link>标签,to 属性指定路由地址
  • <Route></Route>:类似于vue中的<router-view></router-view>,指定路由内容(组件)展示位置

路由参数

  • 配置:通过Route中的path属性来配置路由参数
  • 获取:this.props.match.params 获取
// 配置路由参数
<Route path="/movie/:movieType"></Route>

// 获取路由参数
const type = this.props.match.params.movieType

路由跳转

  • react router - history
  • history.push() 方法用于在JS中实现页面跳转
  • history.go(-1) 用来实现页面的前进(1)和后退(-1)
this.props.history.push('/movie/movieDetail/' + movieId)

fetch

  • 作用:Fetch 是一个现代的概念, 等同于 XMLHttpRequest。它提供了许多与XMLHttpRequest相同的功能,但被设计成更具可扩展性和高效性。
  • fetch() 方法返回一个Promise对象

fetch 基本使用

/*
  通过fetch请求回来的数据,是一个Promise对象.
  调用then()方法,通过参数response,获取到响应对象
  调用 response.json() 方法,解析服务器响应数据
  再次调用then()方法,通过参数data,就获取到数据了
*/
fetch('/api/movie/' + this.state.movieType)
  // response.json() 读取response对象,并返回一个被解析为JSON格式的promise对象
  .then((response) => response.json())
  // 通过 data 获取到数据
  .then((data) => {
    console.log(data);
    this.setState({
      movieList: data.subjects,
      loaing: false
    })
  })

跨域获取数据的三种常用方式

  • 1 JSONP
  • 2 代理
  • 3 CORS

JSONP

  • 安装:npm i -S fetch-jsonp
  • 利用JSONP实现跨域获取数据,只能获取GET请求
  • fetch-jsonp
  • fetch-jsonp
  • 限制:1 只能发送GET请求 2 需要服务端支持JSONP请求
/* movielist.js */
fetchJsonp('https://api.douban.com/v2/movie/in_theaters')
  .then(rep => rep.json())
  .then(data => { console.log(data) })

代理

  • webpack-dev-server 代理配置如下:
  • 问题:webpack-dev-server 是开发期间使用的工具,项目上线了就不再使用 webpack-dev-server
  • 解决:项目上线后的代码,也是会部署到一个服务器中,这个服务器配置了代理功能即可(要求两个服务器中配置的代理规则相同)
// webpack-dev-server的配置
devServer: {
  // https://webpack.js.org/configuration/dev-server/#devserver-proxy
  // https://github.com/chimurai/http-proxy-middleware#http-proxy-options
  // http://www.jianshu.com/p/3bdff821f859
  proxy: {
    // 使用:/api/movie/in_theaters
    // 访问 ‘/api/movie/in_theaters’ ==> 'https://api.douban.com/v2/movie/in_theaters'
    '/api': {
      // 代理的目标服务器地址
      target: 'https://api.douban.com/v2',
      // https请求需要该设置
      secure: false,
      // 必须设置该项
      changeOrigin: true,
      // '/api/movie/in_theaters' 路径重写为:'/movie/in_theaters'
      pathRewrite: {"^/api" : ""}
    }
  }
}

/* movielist.js */
fetch('/api/movie/in_theaters')
  .then(function(data) {
    // 将服务器返回的数据转化为 json 格式
    return data.json()
  })
  .then(function(rep) {
    // 获取上面格式化后的数据
    console.log(rep);
  })

CORS - 服务器端配合

// 通过Express的中间件来处理所有请求
app.use('*', function (req, res, next) {
  // 设置请求头为允许跨域
  res.header('Access-Control-Allow-Origin', '*');

  // 设置服务器支持的所有头信息字段
  res.header('Access-Control-Allow-Headers', 'Content-Type,Content-Length, Authorization,Accept,X-Requested-With');
  // 设置服务器支持的所有跨域请求的方法
  res.header('Access-Control-Allow-Methods', 'POST,GET');
  // next()方法表示进入下一个路由
  next();
});

redux

  • 状态管理工具,用来管理应用中的数据

核心

  • Action:行为的抽象,视图中的每个用户交互都是一个action

    • 比如:点击按钮
  • Reducer:行为响应的抽象,也就是:根据action行为,执行相应的逻辑操作,更新state

    • 比如:点击按钮后,添加任务,那么,添加任务这个逻辑放到 Reducer 中
    • 1 创建State
  • Store:

    • 1 Redux应用只能有一个store
    • 2 getState():获取state
    • 3 dispatch(action):更新state
/* action */

// 在 redux 中,action 就是一个对象
// action 必须提供一个:type属性,表示当前动作的标识
// 其他的参数:表示这个动作需要用到的一些数据
{ type: 'ADD_TODO', name: '要添加的任务名称' }

// 这个动作表示要切换任务状态
{ type: 'TOGGLE_TODO', id: 1 }
/* reducer */

// 第一个参数:表示状态(数据),我们需要给初始状态设置默认值
// 第二个参数:表示 action 行为
function todo(state = [], action) {
  switch(action.type) {
    case 'ADD_TODO':
      state.push({ id: Math.random(), name: action.name, completed: false })
      return state
    case 'TOGGLE_TODO':
      for(var i = 0; i < state.length; i++) {
        if (state[i].id === action.id) {
          state[i].completed = !state[i].completed
          break
        }
      }
      return state
    default:
      return state
  }
}

// 要执行 ADD_TODO 这个动作:
dispatch( { type: 'ADD_TODO', name: '要添加的任务名称' } )

// 内部会调用 reducer
todo(undefined, { type: 'ADD_TODO', name: '要添加的任务名称' })

同事的博客各种干货值得收藏

查看原文

赞 546 收藏 431 评论 26

zhouciming 收藏了文章 · 2019-11-25

React入门看这篇就够了

react - JSX

React 背景介绍

React 起源于 Facebook 的内部项目,因为该公司对市场上所有 JavaScript MVC 框架,都不满意,就决定自己写一套,用来架设 Instagram 的网站。做出来以后,发现这套东西很好用,就在2013年5月开源了。

什么是React

  • A JAVASCRIPT LIBRARY FOR BUILDING USER INTERFACES

    • 用来构建UI的 JavaScript库
    • React 不是一个 MVC 框架,仅仅是视图(V)层的库
  • React 官网
  • React 中文文档

特点

  • 1 使用 JSX语法 创建组件,实现组件化开发,为函数式的 UI 编程方式打开了大门
  • 2 性能高的让人称赞:通过 diff算法虚拟DOM 实现视图的高效更新
  • 3 HTML仅仅是个开始
> JSX --TO--> EveryThing

- JSX --> HTML
- JSX --> native ios或android中的组件(XML)
- JSX --> VR
- JSX --> 物联网

为什么要用React

  • 1 使用组件化开发方式,符合现代Web开发的趋势
  • 2 技术成熟,社区完善,配件齐全,适用于大型Web项目(生态系统健全)
  • 3 由Facebook专门的团队维护,技术支持可靠
  • 4 ReactNative - Learn once, write anywhere: Build mobile apps with React
  • 5 使用方式简单,性能非常高,支持服务端渲染
  • 6 React非常火,从技术角度,可以满足好奇心,提高技术水平;从职业角度,有利于求职和晋升,有利于参与潜力大的项目

React中的核心概念

  • 1 虚拟DOM(Virtual DOM)
  • 2 Diff算法(虚拟DOM的加速器,提升React性能的法宝)

虚拟DOM(Vitural DOM)

React将DOM抽象为虚拟DOM,虚拟DOM其实就是用一个对象来描述DOM,通过对比前后两个对象的差异,最终只把变化的部分重新渲染,提高渲染的效率

为什么用虚拟dom,当dom反生更改时需要遍历 而原生dom可遍历属性多大231个 且大部分与渲染无关 更新页面代价太大

VituralDOM的处理方式

  • 1 用 JavaScript 对象结构表示 DOM 树的结构,然后用这个树构建一个真正的 DOM 树,插到文档当中
  • 2 当状态变更的时候,重新构造一棵新的对象树。然后用新的树和旧的树进行比较,记录两棵树差异
  • 3 把2所记录的差异应用到步骤1所构建的真正的DOM树上,视图就更新了

Diff算法

当你使用React的时候,在某个时间点 render() 函数创建了一棵React元素树,
在下一个state或者props更新的时候,render() 函数将创建一棵新的React元素树,
React将对比这两棵树的不同之处,计算出如何高效的更新UI(只更新变化的地方)
<!-- 了解:

有一些解决将一棵树转换为另一棵树的最小操作数算法问题的通用方案。然而,树中元素个数为n,最先进的算法 的时间复杂度为O(n3) 。
如果直接使用这个算法,在React中展示1000个元素则需要进行10亿次的比较。这操作太过昂贵,相反,React基于两点假设,实现了一个O(n)算法,提升性能: -->
  • React中有两种假定:

    • 1 两个不同类型的元素会产生不同的树(根元素不同结构树一定不同)
    • 2 开发者可以通过key属性指定不同树中没有发生改变的子元素

Diff算法的说明 - 1

  • 如果两棵树的根元素类型不同,React会销毁旧树,创建新树
// 旧树
<div>
  <Counter />
</div>

// 新树
<span>
  <Counter />
</span>

执行过程:destory Counter -> insert Counter

Diff算法的说明 - 2

  • 对于类型相同的React DOM 元素,React会对比两者的属性是否相同,只更新不同的属性
  • 当处理完这个DOM节点,React就会递归处理子节点。
// 旧
<div className="before" title="stuff" />
// 新
<div className="after" title="stuff" />
只更新:className 属性

// 旧
<div style={{color: 'red', fontWeight: 'bold'}} />
// 新
<div style={{color: 'green', fontWeight: 'bold'}} />
只更新:color属性

Diff算法的说明 - 3

  • 1 当在子节点的后面添加一个节点,这时候两棵树的转化工作执行的很好
// 旧
<ul>
  <li>first</li>
  <li>second</li>
</ul>

// 新
<ul>
  <li>first</li>
  <li>second</li>
  <li>third</li>
</ul>

执行过程:
React会匹配新旧两个<li>first</li>,匹配两个<li>second</li>,然后添加 <li>third</li> tree
  • 2 但是如果你在开始位置插入一个元素,那么问题就来了:
// 旧
<ul>
  <li>Duke</li>
  <li>Villanova</li>
</ul>

// 新
<ul>
  <li>Connecticut</li>
  <li>Duke</li>
  <li>Villanova</li>
</ul>

在没有key属性时执行过程:
React将改变每一个子删除重新创建,而非保持 <li>Duke</li> 和 <li>Villanova</li> 不变

key 属性

为了解决以上问题,React提供了一个 key 属性。当子节点带有key属性,React会通过key来匹配原始树和后来的树。
// 旧
<ul>
  <li key="2015">Duke</li>
  <li key="2016">Villanova</li>
</ul>

// 新
<ul>
  <li key="2014">Connecticut</li>
  <li key="2015">Duke</li>
  <li key="2016">Villanova</li>
</ul>
执行过程:
现在 React 知道带有key '2014' 的元素是新的,对于 '2015' 和 '2016' 仅仅移动位置即可 
  • 说明:key属性在React内部使用,但不会传递给你的组件
  • 推荐:在遍历数据时,推荐在组件中使用 key 属性:<li key={item.id}>{item.name}</li>
  • 注意:key只需要保持与他的兄弟节点唯一即可,不需要全局唯一
  • 注意:尽可能的减少数组index作为key,数组中插入元素的等操作时,会使得效率底下

React的基本使用

  • 安装:npm i -S react react-dom
  • react:react 是React库的入口点
  • react-dom:提供了针对DOM的方法,比如:把创建的虚拟DOM,渲染到页面上
// 1. 导入 react
import React from 'react'
import ReactDOM from 'react-dom'

// 2. 创建 虚拟DOM
// 参数1:元素名称  参数2:元素属性对象(null表示无)  参数3:当前元素的子元素string||createElement() 的返回值
const divVD = React.createElement('div', {
  title: 'hello react'
}, 'Hello React!!!')

// 3. 渲染
// 参数1:虚拟dom对象  参数2:dom对象表示渲染到哪个元素内 参数3:回调函数
ReactDOM.render(divVD, document.getElementById('app'))

createElement()的问题

  • 说明:createElement()方式,代码编写不友好,太复杂
var dv = React.createElement(
  "div",
  { className: "shopping-list" },
  React.createElement(
    "h1",
    null,
    "Shopping List for "
  ),
  React.createElement(
    "ul",
    null,
    React.createElement(
      "li",
      null,
      "Instagram"
    ),
    React.createElement(
      "li",
      null,
      "WhatsApp"
    )
  )
)
// 渲染
ReactDOM.render(dv, document.getElementById('app'))

JSX 的基本使用

  • 注意:JSX语法,最终会被编译为 createElement() 方法
  • 推荐:使用 JSX 的方式创建组件
  • JSX - JavaScript XML
  • 安装:npm i -D babel-preset-react (依赖与:babel-core/babel-loader)
注意:JSX的语法需要通过 babel-preset-react 编译后,才能被解析执行
/* 1 在 .babelrc 开启babel对 JSX 的转换 */
{
  "presets": [
    "env", "react"
  ]
}

/* 2 webpack.config.js */
module: [
  rules: [
    { test: /\.js$/, use: 'babel-loader', exclude: /node_modules/ },
  ]
]

/* 3 在 js 文件中 使用 JSX */
const dv = (
  <div title="标题" className="cls container">Hello JSX!</div>
)

/* 4 渲染 JSX 到页面中 */
ReactDOM.render(dv, document.getElementById('app'))

JSX的注意点

  • 注意 1: 如果在 JSX 中给元素添加类, 需要使用 className 代替 class

    • 类似:label 的 for属性,使用htmlFor代替
  • 注意 2:在 JSX 中可以直接使用 JS代码,直接在 JSX 中通过 {} 中间写 JS代码即可
  • 注意 3:在 JSX 中只能使用表达式,但是不能出现 语句!!!
  • 注意 4:在 JSX 中注释语法:{/* 中间是注释的内容 */}

React组件

React 组件可以让你把UI分割为独立、可复用的片段,并将每一片段视为相互独立的部分。
  • 组件是由一个个的HTML元素组成的
  • 概念上来讲, 组件就像JS中的函数。它们接受用户输入(props),并且返回一个React对象,用来描述展示在页面中的内容

React创建组件的两种方式

  • 1 通过 JS函数 创建(无状态组件)
  • 2 通过 class 创建(有状态组件)
函数式组件 和 class 组件的使用场景说明:
1 如果一个组件仅仅是为了展示数据,那么此时就可以使用 函数组件
2 如果一个组件中有一定业务逻辑,需要操作数据,那么就需要使用 class 创建组件,因为,此时需要使用 state

JavaScript函数创建

  • 注意:1 函数名称必须为大写字母开头,React通过这个特点来判断是不是一个组件
  • 注意:2 函数必须有返回值,返回值可以是:JSX对象或null
  • 注意:3 返回的JSX,必须有一个根元素
  • 注意:4 组件的返回值使用()包裹,避免换行问题
function Welcome(props) {
  return (
    // 此处注释的写法 
    <div className="shopping-list">
      {/* 此处 注释的写法 必须要{}包裹 */}
      <h1>Shopping List for {props.name}</h1>
      <ul>
        <li>Instagram</li>
        <li>WhatsApp</li>
      </ul>
    </div>
  )
}

ReactDOM.render(
  <Welcome name="jack" />,
  document.getElementById('app')
)

class创建

在es6中class仅仅是一个语法糖,不是真正的类,本质上还是构造函数+原型 实现继承
// ES6中class关键字的简单使用

// - **ES6中的所有的代码都是运行在严格模式中的**
// - 1 它是用来定义类的,是ES6中实现面向对象编程的新方式
// - 2 使用`static`关键字定义静态属性
// - 3 使用`constructor`构造函数,创建实例属性
// - [参考](http://es6.ruanyifeng.com/#docs/class)

// 语法:
class Person {
  // 实例的构造函数 constructor
  constructor(age){
    // 实例属性
    this.age = age
  }
  // 在class中定义方法 此处为实例方法 通过实例打点调用
  sayHello () {
    console.log('大家好,我今年' + this.age + '了');
  }

  // 静态方法 通过构造函数打点调用 Person.doudou()
  static doudou () {
    console.log('我是小明,我新get了一个技能,会暖床');
  }
}
// 添加静态属性
Person.staticName = '静态属性'
// 实例化对象
const p = new Person(19)
 
 
// 实现继承的方式
 
class American extends Person {
  constructor() {
    // 必须调用super(), super表示父类的构造函数
    super()
    this.skin = 'white'
    this.eyeColor = 'white'
  }
}

// 创建react对象
// 注意:基于 `ES6` 中的class,需要配合 `babel` 将代码转化为浏览器识别的ES5语法
// 安装:`npm i -D babel-preset-env`
 
//  react对象继承字React.Component
class ShoppingList extends React.Component {
  constructor(props) { 
    super(props)
  }
  // class创建的组件中 必须有rander方法 且显示return一个react对象或者null
  render() {
    return (
      <div className="shopping-list">
        <h1>Shopping List for {this.props.name}</h1>
        <ul>
          <li>Instagram</li>
          <li>WhatsApp</li>
        </ul>
      </div>
    )
  }
}

给组件传递数据 - 父子组件传递数据

  • 组件中有一个 只读的对象 叫做 props,无法给props添加属性
  • 获取方式:函数参数 props
  • 作用:将传递给组件的属性转化为 props 对象中的属性
function Welcome(props){
  // props ---> { username: 'zs', age: 20 }
  return (
    <div>
      <div>Welcome React</div>
      <h3>姓名:{props.username}----年龄是:{props.age}</h3>
    </div>
  )
}

// 给 Hello组件 传递 props:username 和 age(如果你想要传递numb类型是数据 就需要向下面这样)
ReactDOM.reander(<Hello username="zs" age={20}></Hello>, ......)

封装组件到独立的文件中

// 创建Hello2.js组件文件
// 1. 引入React模块
// 由于 JSX 编译后会调用 React.createElement 方法,所以在你的 JSX 代码中必须首先拿到React。
import React from 'react'

// 2. 使用function构造函数创建组件
function Hello2(props){
  return (
    <div>
      <div>这是Hello2组件</div>
      <h1>这是大大的H1标签,我大,我骄傲!!!</h1>
      <h6>这是小小的h6标签,我小,我傲娇!!!</h6>
    </div>
  )
}
// 3. 导出组件
export default Hello2

// app.js中   使用组件:
import Hello2 from './components/Hello2'

props和state

props

  • 作用:给组件传递数据,一般用在父子组件之间
  • 说明:React把传递给组件的属性转化为一个对象并交给 props
  • 特点:props是只读的,无法给props添加或修改属性
  • props.children:获取组件的内容,比如:

    • <Hello>组件内容</Hello> 中的 组件内容
// props 是一个包含数据的对象参数,不要试图修改 props 参数
// 返回值:react元素
function Welcome(props) {
  // 返回的 react元素中必须只有一个根元素
  return <div>hello, {props.name}</div>
}

class Welcome extends React.Component {
  constructor(props) {
    super(props)
  }

  render() {
    return <h1>Hello, {this.props.name}</h1>
  }
}

state

状态即数据
  • 作用:用来给组件提供组件内部使用的数据
  • 注意:只有通过class创建的组件才具有状态
  • 注意:状态是私有的,完全由组件来控制
  • 注意:不要在 state 中添加 render() 方法中不需要的数据,会影响渲染性能!

    • 可以将组件内部使用但是不渲染在视图中的内容,直接添加给 this
  • 注意:不要在 render() 方法中调用 setState() 方法来修改state的值

    • 但是可以通过 this.state.name = 'rose' 方式设置state(不推荐!!!!)
// 例:
class Hello extends React.Component {
  constructor() {
    // es6继承必须用super调用父类的constructor
    super()

    this.state = {
      gender: 'male'
    }
  }

  render() {
    return (
      <div>性别:{ this.state.gender }</div>
    )
  }
}

JSX语法转化过程

// 1、JSX
const element = (
  <h1 className="greeting">
    Hello, world!
  </h1>
)

// 2、JSX -> createElement
const element = React.createElement(
  'h1',
  {className: 'greeting'},
  'Hello, world!'
)

// React elements: 使用对象的形式描述页面结构
// Note: 这是简化后的对象结构
const element = {
  type: 'h1',
  props: {
    className: 'greeting',
  },
  children: ['Hello, world']
}

评论列表案例

  • 巩固有状态组件和无状态组件的使用
  • 两个组件:<CommentList></CommentList><Comment></Comment>
[
  { user: '张三', content: '哈哈,沙发' },
  { user: '张三2', content: '哈哈,板凳' },
  { user: '张三3', content: '哈哈,凉席' },
  { user: '张三4', content: '哈哈,砖头' },
  { user: '张三5', content: '哈哈,楼下山炮' }
]

// 属性扩展
<Comment {...item} key={i}></Comment>

style样式

// 1. 直接写行内样式:
<li style={{border:'1px solid red', fontSize:'12px'}}></li>

// 2. 抽离为对象形式
var styleH3 = {color:'blue'}
var styleObj = {
  liStyle:{border:'1px solid red', fontSize:'12px'},
  h3Style:{color:'green'}
}

<li style={styleObj.liStyle}>
  <h3 style={styleObj.h3Style}>评论内容:{props.content}</h3>
</li>

// 3. 使用样式表定义样式:
import '../css/comment.css'
<p className="pUser">评论人:{props.user}</p>

相关文章

组件的生命周期

  • 简单说:一个组件从开始到最后消亡所经历的各种状态,就是一个组件的生命周期

组件生命周期函数的定义:从组件被创建,到组件挂载到页面上运行,再到页面关闭组件被卸载,这三个阶段总是伴随着组件各种各样的事件,那么这些事件,统称为组件的生命周期函数!

组件生命周期函数总览

  • 组件的生命周期包含三个阶段:创建阶段(Mounting)、运行和交互阶段(Updating)、卸载阶段(Unmounting)
  • Mounting:
constructor()
componentWillMount()
render()
componentDidMount()
  • Updating
componentWillReceiveProps()
shouldComponentUpdate()
componentWillUpdate()
render()
componentDidUpdate()
  • Unmounting
componentWillUnmount()

组件生命周期 - 创建阶段(Mounting)

  • 特点:该阶段的函数只执行一次

constructor()

  • 作用:1 获取props 2 初始化state
  • 说明:通过 constructor() 的参数props获取
  • 设置state和props
class Greeting extends React.Component {
  constructor(props) {
    // 获取 props
    super(props)
    // 初始化 state
    this.state = {
      count: props.initCount
    }
  }
}

// 初始化 props
// 语法:通过静态属性 defaultProps 来初始化props
Greeting.defaultProps = {
  initCount: 0
};

componentWillMount()

  • 说明:组件被挂载到页面之前调用,其在render()之前被调用,因此在这方法里同步地设置状态将不会触发重渲染
  • 注意:无法获取页面中的DOM对象
  • 注意:可以调用 setState() 方法来改变状态值
  • 用途:发送ajax请求获取数据
componentWillMount() {
  console.warn(document.getElementById('btn')) // null
  this.setState({
    count: this.state.count + 1
  })
}

render()

  • 作用:渲染组件到页面中,无法获取页面中的DOM对象
  • 注意:不要在render方法中调用 setState() 方法,否则会递归渲染

    • 原因说明:状态改变会重新调用render()render()又重新改变状态
render() {
  console.warn(document.getElementById('btn')) // null

  return (
    <div>
      <button id="btn" onClick={this.handleAdd}>打豆豆一次</button>
      {
        this.state.count === 4
        ? null
        : <CounterChild initCount={this.state.count}></CounterChild>
      }
    </div>
  )
}

componentDidMount()

  • 1 组件已经挂载到页面中
  • 2 可以进行DOM操作,比如:获取到组件内部的DOM对象
  • 3 可以发送请求获取数据
  • 4 可以通过 setState() 修改状态的值
  • 注意:在这里修改状态会重新渲染
componentDidMount() {
  // 此时,就可以获取到组件内部的DOM对象
  console.warn('componentDidMount', document.getElementById('btn'))
}

组件生命周期 - 运行阶段(Updating)

  • 特点:该阶段的函数执行多次
  • 说明:每当组件的props或者state改变的时候,都会触发运行阶段的函数

componentWillReceiveProps()

  • 说明:组件接受到新的props前触发这个方法
  • 参数:当前组件props
  • 可以通过 this.props 获取到上一次的值
  • 使用:若你需要响应属性的改变,可以通过对比this.propsnextProps并在该方法中使用this.setState()处理状态改变
  • 注意:修改state不会触发该方法
componentWillReceiveProps(nextProps) {
  console.warn('componentWillReceiveProps', nextProps)
}

shouldComponentUpdate()

  • 作用:根据这个方法的返回值决定是否重新渲染组件,返回true重新渲染,否则不渲染
  • 优势:通过某个条件渲染组件,降低组件渲染频率,提升组件性能
  • 说明:如果返回值为false,那么,后续render()方法不会被调用
  • 注意:这个方法必须返回布尔值!!!
  • 场景:根据随机数决定是否渲染组件
// - 参数:
//   - 第一个参数:最新属性对象
//   - 第二个参数:最新状态对象
shouldComponentUpdate(nextProps, nextState) {
  console.warn('shouldComponentUpdate', nextProps, nextState)

  return nextState.count % 2 === 0
}

componentWillUpdate()

  • 作用:组件将要更新
  • 参数:最新的属性和状态对象
  • 注意:不能修改状态 否则会循环渲染
componentWillUpdate(nextProps, nextState) {
  console.warn('componentWillUpdate', nextProps, nextState)
}

render() 渲染

  • 作用:重新渲染组件,与Mounting阶段的render是同一个函数
  • 注意:这个函数能够执行多次,只要组件的属性或状态改变了,这个方法就会重新执行

componentDidUpdate()

  • 作用:组件已经被更新
  • 参数:旧的属性和状态对象
componentDidUpdate(prevProps, prevState) {
  console.warn('componentDidUpdate', prevProps, prevState)
}

组件生命周期 - 卸载阶段(Unmounting)

  • 组件销毁阶段:组件卸载期间,函数比较单一,只有一个函数,这个函数也有一个显著的特点:组件一辈子只能执行依次!
  • 使用说明:只要组件不再被渲染到页面中,那么这个方法就会被调用( 渲染到页面中 -> 不再渲染到页面中 )

componentWillUnmount()

  • 作用:在卸载组件的时候,执行清理工作,比如

    • 1 清除定时器
    • 2 清除componentDidMount创建的DOM对象

React - createClass(不推荐)

  • React.createClass({}) 方式,创建有状态组件,该方式已经被废弃!!!
  • 通过导入 require('create-react-class'),可以在不适用ES6的情况下,创建有状态组件
  • getDefaultProps() 和 getInitialState() 方法:是 createReactClass() 方式创建组件中的两个函数
  • React without ES6
  • React 不适用ES6
var createReactClass = require('create-react-class');
var Greeting = createReactClass({
  // 初始化 props
  getDefaultProps: function() {
    console.log('getDefaultProps');
    return {
      title: 'Basic counter!!!'
    }
  },

  // 初始化 state
  getInitialState: function() {
    console.log('getInitialState');
    return {
      count: 0
    }
  },

  render: function() {
    console.log('render');
    return (
      <div>
        <h1>{this.props.title}</h1>
        <div>{this.state.count}</div>
        <input type='button' value='+' onClick={this.handleIncrement} />
      </div>
    );
  },

  handleIncrement: function() {
    var newCount = this.state.count + 1;
    this.setState({count: newCount});
  },

  propTypes: {
    title: React.PropTypes.string
  }
});

ReactDOM.render(
  React.createElement(Greeting),
  document.getElementById('app')
);

state和setState

  • 注意:使用 setState() 方法修改状态,状态改变后,React会重新渲染组件
  • 注意:不要直接修改state属性的值,这样不会重新渲染组件!!!
  • 使用:1 初始化state 2 setState修改state
// 修改state(不推荐使用)
// https://facebook.github.io/react/docs/state-and-lifecycle.html#do-not-modify-state-directly
this.state.test = '这样方式,不会重新渲染组件';
constructor(props) {
  super(props)

  // 正确姿势!!!
  // -------------- 初始化 state --------------
  this.state = {
    count: props.initCount
  }
}

componentWillMount() {
  // -------------- 修改 state 的值 --------------
  // 方式一:
  this.setState({
    count: this.state.count + 1
  })

  this.setState({
    count: this.state.count + 1
  }, function(){
    // 由于 setState() 是异步操作,所以,如果想立即获取修改后的state
    // 需要在回调函数中获取
    // https://doc.react-china.org/docs/react-component.html#setstate
  });

  // 方式二:
  this.setState(function(prevState, props) {
    return {
      counter: prevState.counter + props.increment
    }
  })

  // 或者 - 注意: => 后面需要带有小括号,因为返回的是一个对象
  this.setState((prevState, props) => ({
    counter: prevState.counter + props.increment
  }))
}

组件绑定事件

  • 1 通过React事件机制 onClick 绑定
  • 2 JS原生方式绑定(通过 ref 获取元素)

    • 注意:ref 是React提供的一个特殊属性
    • ref的使用说明:react ref

React中的事件机制 - 推荐

  • 注意:事件名称采用驼峰命名法
  • 例如:onClick 用来绑定单击事件
<input type="button" value="触发单击事件"
  onClick={this.handleCountAdd}
  onMouseEnter={this.handleMouseEnter}
/>

JS原生方式 - 知道即可

  • 说明:给元素添加 ref 属性,然后,获取元素绑定事件
// JSX
// 将当前DOM的引用赋值给 this.txtInput 属性
<input ref={ input => this.txtInput = input } type="button" value="我是豆豆" />

componentDidMount() {
  // 通过 this.txtInput 属性获取元素绑定事件
  this.txtInput.addEventListener(() => {
    this.setState({
      count:this.state.count + 1
    })
  })
}

事件绑定中的this

  • 1 通过 bind 绑定
  • 2 通过 箭头函数 绑定

通过bind绑定

  • 原理:bind能够调用函数,改变函数内部this的指向,并返回一个新函数
  • 说明:bind第一个参数为返回函数中this的指向,后面的参数为传给返回函数的参数
// 自定义方法:
handleBtnClick(arg1, arg2) {
  this.setState({
    msg: '点击事件修改state的值' + arg1 + arg2
  })
}

render() {
  return (
    <div>
      <button onClick={
        // 无参数
        // this.handleBtnClick.bind(this)

        // 有参数
        this.handleBtnClick.bind(this, 'abc', [1, 2])
      }>事件中this的处理</button>
      <h1>{this.state.msg}</h1>
    </div>
  )
}
  • 在构造函数中使用bind
constructor() {
  super()

  this.handleBtnClick = this.handleBtnClick.bind(this)
}

// render() 方法中:
<button onClick={ this.handleBtnClick }>事件中this的处理</button>

通过箭头函数绑定

  • 原理:箭头函数中的this由所处的环境决定,自身不绑定this
<input type="button" value="在构造函数中绑定this并传参" onClick={
  () => { this.handleBtnClick('参数1', '参数2') }
} />

handleBtnClick(arg1, arg2) {
  this.setState({
    msg: '在构造函数中绑定this并传参' + arg1 + arg2
  });
}

受控组件

在HTML当中,像input,textareaselect这类表单元素会维持自身状态,并根据用户输入进行更新。
在React中,可变的状态通常保存在组件的state中,并且只能用 setState() 方法进行更新.
React根据初始状态渲染表单组件,接受用户后续输入,改变表单组件内部的状态。
因此,将那些值由React控制的表单元素称为:受控组件。
  • 受控组件的特点:

    • 1 表单元素
    • 2 由React通过JSX渲染出来
    • 3 由React控制值的改变,也就是说想要改变元素的值,只能通过React提供的方法来修改
  • 注意:只能通过setState来设置受控组件的值
// 模拟实现文本框数据的双向绑定
<input type="text" value={this.state.msg} onChange={this.handleTextChange}/>

// 当文本框内容改变的时候,触发这个事件,重新给state赋值
handleTextChange = event => {
  console.log(event.target.value)

  this.setState({
    msg: event.target.value
  })
}

评论列表案例

[
  {name: '小明', content: '沙发!!!'},
  {name: '小红', content: '小明,居然是你'},
  {name: '小刚', content: '小明,放学你别走!!!'},
]

props校验

  • 作用:通过类型检查,提高程序的稳定性
  • 命令:npm i -S prop-types
  • 类型校验文档
  • 使用:给类提供一个静态属性 propTypes(对象),来约束props
// 引入模块
import PropTypes from 'prop-types'

// ...以下代码是类的静态属性:
// propTypes 静态属性的名称是固定的!!!
static propTypes = {
  initCount: PropTypes.number, // 规定属性的类型
  initAge: PropTypes.number.isRequired // 规定属性的类型,且规定为必传字段
}

React 单向数据流

  • React 中采用单项数据流
  • 数据流动方向:自上而下,也就是只能由父组件传递到子组件
  • 数据都是由父组件提供的,子组件想要使用数据,都是从父组件中获取的
  • 如果多个组件都要使用某个数据,最好将这部分共享的状态提升至他们最近的父组件当中进行管理
  • 单向数据流
  • 状态提升
react中的单向数据流动:
1 数据应该是从上往下流动的,也就是由父组件将数据传递给子组件
2 数据应该是由父组件提供,子组件要使用数据的时候,直接从子组件中获取

在我们的评论列表案例中:数据是由CommentList组件(父组件)提供的
子组件 CommentItem 负责渲染评论列表,数据是由 父组件提供的
子组件 CommentForm 负责获取用户输入的评论内容,最终也是把用户名和评论内容传递给了父组件,由父组件负责处理这些数据( 把数据交给 CommentItem 由这个组件负责渲染 )

组件通讯

  • 父 -> 子:props
  • 子 -> 父:父组件通过props传递回调函数给子组件,子组件调用函数将数据作为参数传递给父组件
  • 兄弟组件:因为React是单向数据流,因此需要借助父组件进行传递,通过父组件回调函数改变兄弟组件的props
  • React中的状态管理: flux(提出状态管理的思想) -> Redux -> mobx
  • Vue中的状态管理: Vuex
  • 简单来说,就是统一管理了项目中所有的数据,让数据变的可控
  • 组件通讯

Context特性

  • 注意:如果不熟悉React中的数据流,不推荐使用这个属性

    • 这是一个实验性的API,在未来的React版本中可能会被更改
  • 作用:跨级传递数据(爷爷给孙子传递数据),避免向下每层手动地传递props
  • 说明:需要配合PropTypes类型限制来使用
class Grandfather extends React.Component {
  // 类型限制(必须),静态属性名称固定
  static childContextTypes = {
    color: PropTypes.string.isRequired
  }

  // 传递给孙子组件的数据
  getChildContext() {
    return {
      color: 'red'
    }
  }

  render() {
    return (
      <Father></Father>
    )
  }
}

class Child extends React.Component {
  // 类型限制,静态属性名字固定
  static contextTypes = {
    color: PropTypes.string
  }

  render() {
    return (
      // 从上下文对象中获取爷爷组件传递过来的数据
      <h1 style={{ color: this.context.color }}>爷爷告诉文字是红色的</h1>
    )
  }
}

class Father extends React.Component {
  render() {
    return (
      <Child></Child>
    )
  }
}

react-router

基本概念说明

  • Router组件本身只是一个容器,真正的路由要通过Route组件定义

使用步骤

  • 1 导入路由组件
  • 2 使用 <Router></Router> 作为根容器,包裹整个应用(JSX)

    • 在整个应用程序中,只需要使用一次
  • 3 使用 <Link to="/movie"></Link> 作为链接地址,并指定to属性
  • 4 使用 <Route path="/" compoent={Movie}></Route> 展示路由内容
// 1 导入组件
import {
  HashRouter as Router,
  Link, Route
} from 'react-router-dom'

// 2 使用 <Router>
<Router>

    // 3 设置 Link
    <Menu.Item key="1"><Link to="/">首页</Link></Menu.Item>
    <Menu.Item key="2"><Link to="/movie">电影</Link></Menu.Item>
    <Menu.Item key="3"><Link to="/about">关于</Link></Menu.Item>

    // 4 设置 Route
    // exact 表示:绝对匹配(完全匹配,只匹配:/)
    <Route exact path="/" component={HomeContainer}></Route>
    <Route path="/movie" component={MovieContainer}></Route>
    <Route path="/about" component={AboutContainer}></Route>

</Router>

注意点

  • <Router></Router>:作为整个组件的根元素,是路由容器,只能有一个唯一的子元素
  • <Link></Link>:类似于vue中的<router-link></router-link>标签,to 属性指定路由地址
  • <Route></Route>:类似于vue中的<router-view></router-view>,指定路由内容(组件)展示位置

路由参数

  • 配置:通过Route中的path属性来配置路由参数
  • 获取:this.props.match.params 获取
// 配置路由参数
<Route path="/movie/:movieType"></Route>

// 获取路由参数
const type = this.props.match.params.movieType

路由跳转

  • react router - history
  • history.push() 方法用于在JS中实现页面跳转
  • history.go(-1) 用来实现页面的前进(1)和后退(-1)
this.props.history.push('/movie/movieDetail/' + movieId)

fetch

  • 作用:Fetch 是一个现代的概念, 等同于 XMLHttpRequest。它提供了许多与XMLHttpRequest相同的功能,但被设计成更具可扩展性和高效性。
  • fetch() 方法返回一个Promise对象

fetch 基本使用

/*
  通过fetch请求回来的数据,是一个Promise对象.
  调用then()方法,通过参数response,获取到响应对象
  调用 response.json() 方法,解析服务器响应数据
  再次调用then()方法,通过参数data,就获取到数据了
*/
fetch('/api/movie/' + this.state.movieType)
  // response.json() 读取response对象,并返回一个被解析为JSON格式的promise对象
  .then((response) => response.json())
  // 通过 data 获取到数据
  .then((data) => {
    console.log(data);
    this.setState({
      movieList: data.subjects,
      loaing: false
    })
  })

跨域获取数据的三种常用方式

  • 1 JSONP
  • 2 代理
  • 3 CORS

JSONP

  • 安装:npm i -S fetch-jsonp
  • 利用JSONP实现跨域获取数据,只能获取GET请求
  • fetch-jsonp
  • fetch-jsonp
  • 限制:1 只能发送GET请求 2 需要服务端支持JSONP请求
/* movielist.js */
fetchJsonp('https://api.douban.com/v2/movie/in_theaters')
  .then(rep => rep.json())
  .then(data => { console.log(data) })

代理

  • webpack-dev-server 代理配置如下:
  • 问题:webpack-dev-server 是开发期间使用的工具,项目上线了就不再使用 webpack-dev-server
  • 解决:项目上线后的代码,也是会部署到一个服务器中,这个服务器配置了代理功能即可(要求两个服务器中配置的代理规则相同)
// webpack-dev-server的配置
devServer: {
  // https://webpack.js.org/configuration/dev-server/#devserver-proxy
  // https://github.com/chimurai/http-proxy-middleware#http-proxy-options
  // http://www.jianshu.com/p/3bdff821f859
  proxy: {
    // 使用:/api/movie/in_theaters
    // 访问 ‘/api/movie/in_theaters’ ==> 'https://api.douban.com/v2/movie/in_theaters'
    '/api': {
      // 代理的目标服务器地址
      target: 'https://api.douban.com/v2',
      // https请求需要该设置
      secure: false,
      // 必须设置该项
      changeOrigin: true,
      // '/api/movie/in_theaters' 路径重写为:'/movie/in_theaters'
      pathRewrite: {"^/api" : ""}
    }
  }
}

/* movielist.js */
fetch('/api/movie/in_theaters')
  .then(function(data) {
    // 将服务器返回的数据转化为 json 格式
    return data.json()
  })
  .then(function(rep) {
    // 获取上面格式化后的数据
    console.log(rep);
  })

CORS - 服务器端配合

// 通过Express的中间件来处理所有请求
app.use('*', function (req, res, next) {
  // 设置请求头为允许跨域
  res.header('Access-Control-Allow-Origin', '*');

  // 设置服务器支持的所有头信息字段
  res.header('Access-Control-Allow-Headers', 'Content-Type,Content-Length, Authorization,Accept,X-Requested-With');
  // 设置服务器支持的所有跨域请求的方法
  res.header('Access-Control-Allow-Methods', 'POST,GET');
  // next()方法表示进入下一个路由
  next();
});

redux

  • 状态管理工具,用来管理应用中的数据

核心

  • Action:行为的抽象,视图中的每个用户交互都是一个action

    • 比如:点击按钮
  • Reducer:行为响应的抽象,也就是:根据action行为,执行相应的逻辑操作,更新state

    • 比如:点击按钮后,添加任务,那么,添加任务这个逻辑放到 Reducer 中
    • 1 创建State
  • Store:

    • 1 Redux应用只能有一个store
    • 2 getState():获取state
    • 3 dispatch(action):更新state
/* action */

// 在 redux 中,action 就是一个对象
// action 必须提供一个:type属性,表示当前动作的标识
// 其他的参数:表示这个动作需要用到的一些数据
{ type: 'ADD_TODO', name: '要添加的任务名称' }

// 这个动作表示要切换任务状态
{ type: 'TOGGLE_TODO', id: 1 }
/* reducer */

// 第一个参数:表示状态(数据),我们需要给初始状态设置默认值
// 第二个参数:表示 action 行为
function todo(state = [], action) {
  switch(action.type) {
    case 'ADD_TODO':
      state.push({ id: Math.random(), name: action.name, completed: false })
      return state
    case 'TOGGLE_TODO':
      for(var i = 0; i < state.length; i++) {
        if (state[i].id === action.id) {
          state[i].completed = !state[i].completed
          break
        }
      }
      return state
    default:
      return state
  }
}

// 要执行 ADD_TODO 这个动作:
dispatch( { type: 'ADD_TODO', name: '要添加的任务名称' } )

// 内部会调用 reducer
todo(undefined, { type: 'ADD_TODO', name: '要添加的任务名称' })

同事的博客各种干货值得收藏

查看原文

zhouciming 赞了文章 · 2019-11-15

vue项目移植tinymce踩坑

2019-2-18

貌似这篇文章帮了大家一些小忙
最近tinymce出5.0版本了,下面的api还是4.x的,新版本可能会有些不适用了,最近业务繁忙,等哪天周末有时间的话我再做点更新 :)


前言

最近因业务需求在项目中嵌入了tinymce这个编辑器,用于满足平台给用户编辑各类新闻内容什么的业务需求,前后也花了不少时间体验和对比了市面上各类开源编辑器。

各大WYSIWYG编辑器的简单比较

UEditor: 因为已经不再维护了,需要大量修改源码,很多都是专门为jsp等服务器渲染项目写的代码需要删除, 然后越删越害怕越删越不敢用,依赖jquery,需要专门用js去parse编辑完成的内容,parse完的内容还可能污染全局css,兼容老浏览器还不错, 但是,我们不怎么考虑兼容IE。所以,告辞。

wangEditor: 中文文档,上手快,依赖jquery,功能少点要花时间去写插件,需要单独为图片上传功能写个接口,老项目忙着上线临时用过,感觉并不适合当前业务这么重的编辑功能于是放弃了。

Quill:api友好, 功能少,需要特定的css去解析文本(这点我不大喜欢),ui好看,适合作为论坛回帖功能使用。

CKEditor: CKEditor目前主流的还是4.x的版本,但是文档看着很瞎眼实在是提不起兴致去配置,草草用了下就放弃了,5.x版本刚从beta结束,需要指定专门的node以及npm版本,虽然功能强大配置灵活ui漂亮不过目前糟糕的兼容性基本是不可能出现在大众视野了。

KingEditor: 丑,不喜欢,不爱用

Draft-js: 知乎最近刚改的文本编辑器就是在draft的基础上开发的,依赖react, 弃。

Medium-editor: 虽然看着感觉很酷炫,但是,不适合我们的业务场景啊, api也简陋可怕。

trix: 嗯,又一个小而美,放弃

Slatereact,放弃

Bootstrap-wysiwygbootstrap, jquery, 放弃

tinymce: 文档好,功能强,bug少,无外部依赖,大家用了都说好,嗯,没错就是它了。

编辑器配置方面只要能看得懂英文耍起来还是比较简单的,适配中碰到的大部分问题都可以通过看文档解决,即便看文档解决不了网上也有大量的文章能告诉你怎么配置能解决。

当然了,主要是我这里需要解决一些别人觉得超简单自己一想都很烦人的需求,比如:

  1. word文档粘贴进来要带格式
  2. 兼容移动端
  3. word文档粘贴进来要正常显示并且还要兼容移动端
  4. 电脑网页里粘贴进来内容要正常显示并且排版还不能乱
  5. 电脑网页拷过来的内容还要兼容到移动端

初始化

因为tinymce的Plugins是按需加载的
为了能先快速上手这个编辑器
就先在vue-cli的index.html中默认塞入一条在线cdn地址

<script data-original="https://cdn.bootcss.com/tinymce/4.7.4/tinymce.min.js"></script>

记得去下载语言包到本地,
然后就在文件内引入

import './zh_CN.js'

后面有机会再写下单独打包的事项,毕竟这货体积还不小。

插入vue组件模板

<template>
  <div>
    <textarea :id= "Id"></textarea>
  </div>
</template>

记得一定要在textarea外面包一层div,不然...你自己试试看就知道了。

组件基础配置

将tinymce通过指定的selector挂载到组件中

<template>
  <div>
    <textarea :id= "Id"></textarea>
  </div>
</template>
<script>
  import './zh_CN.js'
  export default {
    data () {
      const Id = Date.now()
      return {
        Id: Id,
        Editor: null,
        DefaultConfig: {}
      }
    },
    props: {
      value: {
        default: '',
        type: String
      },
      config: {
        type: Object,
        default: () => {
          return {
            theme: 'modern',
            height: 300
          }
        }
      }
    },
    mounted () {
      this.init()
    },
    beforeDestroy () {
      // 销毁tinymce
      this.$emit('on-destroy')
      window.tinymce.remove(`#${this.Id}`)
    },
    methods: {
       init () {
        const self = this
        this.Editor = window.tinymce.init({
          // 默认配置
          ...this.DefaultConfig,
          
          // prop内传入的的config
          ...this.config, 
          
          // 挂载的DOM对象
          selector: `#${this.Id}`,
          
          setup: (editor) => {
            // 抛出 'on-ready' 事件钩子
            editor.on(
              'init', () => {
                self.loading = false
                self.$emit('on-ready')
                editor.setContent(self.value)
              }
            )
            // 抛出 'input' 事件钩子,同步value数据
            editor.on(
              'input change undo redo', () => {
                self.$emit('input', editor.getContent())
              }
            )
          }
        })
      }
    }
  }
</script>

好了,组件基本的初始化完成,后面正式开始踩坑之旅

API

具体内容看官网的API就行,英语不好的用chrome翻译下对照着demo也能看个七七八八,当然主要原因还是我比较懒。

我这边根据自身业务需求在组件的data内写了个默认配置


DefaultConfig: {
  // GLOBAL
  height: 500,
  theme: 'modern',
  menubar: false,
  toolbar: `styleselect | fontselect | formatselect | fontsizeselect | forecolor backcolor | bold italic underline strikethrough | image  media | table | alignleft aligncenter alignright alignjustify | outdent indent | numlist bullist | preview removeformat  hr | paste code  link | undo redo | fullscreen `,
  plugins: `
    paste
    importcss
    image
    code
    table
    advlist
    fullscreen
    link
    media
    lists
    textcolor
    colorpicker
    hr
    preview
  `,

  
  // CONFIG

  forced_root_block: 'p',
  force_p_newlines: true,
  importcss_append: true,

 // CONFIG: ContentStyle 这块很重要, 在最后呈现的页面也要写入这个基本样式保证前后一致, `table`和`img`的问题基本就靠这个来填坑了
  content_style: `
    *                         { padding:0; margin:0; }
    html, body                { height:100%; }
    img                       { max-width:100%; display:block;height:auto; }
    a                         { text-decoration: none; }
    iframe                    { width: 100%; }
    p                         { line-height:1.6; margin: 0px; }
    table                     { word-wrap:break-word; word-break:break-all; max-width:100%; border:none; border-color:#999; }
    .mce-object-iframe        { width:100%; box-sizing:border-box; margin:0; padding:0; }
    ul,ol                     { list-style-position:inside; }
  `,

  insert_button_items: 'image link | inserttable',

  // CONFIG: Paste
  paste_retain_style_properties: 'all',
  paste_word_valid_elements: '*[*]',        // word需要它
  paste_data_images: true,                  // 粘贴的同时能把内容里的图片自动上传,非常强力的功能
  paste_convert_word_fake_lists: false,     // 插入word文档需要该属性
  paste_webkit_styles: 'all',
  paste_merge_formats: true,
  nonbreaking_force_tab: false,
  paste_auto_cleanup_on_paste: false,

  // CONFIG: Font
  fontsize_formats: '10px 11px 12px 14px 16px 18px 20px 24px',

  // CONFIG: StyleSelect
  style_formats: [
    {
      title: '首行缩进',
      block: 'p',
      styles: { 'text-indent': '2em' }
    },
    {
      title: '行高',
      items: [
        {title: '1', styles: { 'line-height': '1' }, inline: 'span'},
        {title: '1.5', styles: { 'line-height': '1.5' }, inline: 'span'},
        {title: '2', styles: { 'line-height': '2' }, inline: 'span'},
        {title: '2.5', styles: { 'line-height': '2.5' }, inline: 'span'},
        {title: '3', styles: { 'line-height': '3' }, inline: 'span'}
      ]
    }
  ],

  // FontSelect
  font_formats: `
    微软雅黑=微软雅黑;
    宋体=宋体;
    黑体=黑体;
    仿宋=仿宋;
    楷体=楷体;
    隶书=隶书;
    幼圆=幼圆;
    Andale Mono=andale mono,times;
    Arial=arial, helvetica,
    sans-serif;
    Arial Black=arial black, avant garde;
    Book Antiqua=book antiqua,palatino;
    Comic Sans MS=comic sans ms,sans-serif;
    Courier New=courier new,courier;
    Georgia=georgia,palatino;
    Helvetica=helvetica;
    Impact=impact,chicago;
    Symbol=symbol;
    Tahoma=tahoma,arial,helvetica,sans-serif;
    Terminal=terminal,monaco;
    Times New Roman=times new roman,times;
    Trebuchet MS=trebuchet ms,geneva;
    Verdana=verdana,geneva;
    Webdings=webdings;
    Wingdings=wingdings,zapf dingbats`,

  // Tab
  tabfocus_elements: ':prev,:next',
  object_resizing: true,

  // Image
  imagetools_toolbar: 'rotateleft rotateright | flipv fliph | editimage imageoptions'
}

因为本人比较懒,以上配置导出的代码可能会有代码注入的风险,建议保存的时候再前后端都做下注入过滤,不过一般数据安全问题主要还是服务器那边的事情?。

后面的图片上传可以单独拆出来做个小配置,直接写到props里好了。

  url: {
    default: '',
    type: String
  },
  accept: {
    default: 'image/jpeg, image/png',
    type: String
  },
  maxSize: {
    default: 2097152,
    type: Number
  },
  withCredentials: {
    default: false,
    type: Boolean
  }

然后把这套东西塞到init配置里


  // 图片上传
  images_upload_handler: function (blobInfo, success, failure) {
    if (blobInfo.blob().size > self.maxSize) {
      failure('文件体积过大')
    }
    
    if (self.accept.indexOf(blobInfo.blob().type) >= 0) {
      uploadPic()
    } else {
      failure('图片格式错误')
    }
    function uploadPic () {
      const xhr = new XMLHttpRequest()
      const formData = new FormData()
      xhr.withCredentials = self.withCredentials
      xhr.open('POST', self.url)
      xhr.onload = function () {

        if (xhr.status !== 200) {
          // 抛出 'on-upload-fail' 钩子
          self.$emit('on-upload-fail')
          failure('上传失败: ' + xhr.status)
          return
        }

        const json = JSON.parse(xhr.responseText)
        // 抛出 'on-upload-success' 钩子
        self.$emit('on-upload-complete' , [
          json, success, failure
        ])
      }
      formData.append('file', blobInfo.blob())
      xhr.send(formData)
    }
  }

至此, 一个组件的封装基本算是完成了

看下初阶成果

<template>
  <div>
    <textarea :id= "Id"></textarea>
  </div>
</template>
<script>
  import './zh_CN.js'
  export default {
    data () {
      const Id = Date.now()
      return {
        Id: Id,
        Editor: null,
        DefaultConfig: {
          // GLOBAL
          height: 500,
          theme: 'modern',
          menubar: false,
          toolbar: `styleselect | fontselect | formatselect | fontsizeselect | forecolor backcolor | bold italic underline strikethrough | image  media | table | alignleft aligncenter alignright alignjustify | outdent indent | numlist bullist | preview removeformat  hr | paste code  link | undo redo | fullscreen `,
          plugins: `
            paste
            importcss
            image
            code
            table
            advlist
            fullscreen
            link
            media
            lists
            textcolor
            colorpicker
            hr
            preview
          `,

          
          // CONFIG

          forced_root_block: 'p',
          force_p_newlines: true,
          importcss_append: true,

        // CONFIG: ContentStyle 这块很重要, 在最后呈现的页面也要写入这个基本样式保证前后一致, `table`和`img`的问题基本就靠这个来填坑了
          content_style: `
            *                         { padding:0; margin:0; }
            html, body                { height:100%; }
            img                       { max-width:100%; display:block;height:auto; }
            a                         { text-decoration: none; }
            iframe                    { width: 100%; }
            p                         { line-height:1.6; margin: 0px; }
            table                     { word-wrap:break-word; word-break:break-all; max-width:100%; border:none; border-color:#999; }
            .mce-object-iframe        { width:100%; box-sizing:border-box; margin:0; padding:0; }
            ul,ol                     { list-style-position:inside; }
          `,

          insert_button_items: 'image link | inserttable',

          // CONFIG: Paste
          paste_retain_style_properties: 'all',
          paste_word_valid_elements: '*[*]',        // word需要它
          paste_data_images: true,                  // 粘贴的同时能把内容里的图片自动上传,非常强力的功能
          paste_convert_word_fake_lists: false,     // 插入word文档需要该属性
          paste_webkit_styles: 'all',
          paste_merge_formats: true,
          nonbreaking_force_tab: false,
          paste_auto_cleanup_on_paste: false,

          // CONFIG: Font
          fontsize_formats: '10px 11px 12px 14px 16px 18px 20px 24px',

          // CONFIG: StyleSelect
          style_formats: [
            {
              title: '首行缩进',
              block: 'p',
              styles: { 'text-indent': '2em' }
            },
            {
              title: '行高',
              items: [
                {title: '1', styles: { 'line-height': '1' }, inline: 'span'},
                {title: '1.5', styles: { 'line-height': '1.5' }, inline: 'span'},
                {title: '2', styles: { 'line-height': '2' }, inline: 'span'},
                {title: '2.5', styles: { 'line-height': '2.5' }, inline: 'span'},
                {title: '3', styles: { 'line-height': '3' }, inline: 'span'}
              ]
            }
          ],

          // FontSelect
          font_formats: `
            微软雅黑=微软雅黑;
            宋体=宋体;
            黑体=黑体;
            仿宋=仿宋;
            楷体=楷体;
            隶书=隶书;
            幼圆=幼圆;
            Andale Mono=andale mono,times;
            Arial=arial, helvetica,
            sans-serif;
            Arial Black=arial black, avant garde;
            Book Antiqua=book antiqua,palatino;
            Comic Sans MS=comic sans ms,sans-serif;
            Courier New=courier new,courier;
            Georgia=georgia,palatino;
            Helvetica=helvetica;
            Impact=impact,chicago;
            Symbol=symbol;
            Tahoma=tahoma,arial,helvetica,sans-serif;
            Terminal=terminal,monaco;
            Times New Roman=times new roman,times;
            Trebuchet MS=trebuchet ms,geneva;
            Verdana=verdana,geneva;
            Webdings=webdings;
            Wingdings=wingdings,zapf dingbats`,

          // Tab
          tabfocus_elements: ':prev,:next',
          object_resizing: true,

          // Image
          imagetools_toolbar: 'rotateleft rotateright | flipv fliph | editimage imageoptions'
        }
      }
    },
    props: {
      value: {
        default: '',
        type: String
      },
      config: {
        type: Object,
        default: () => {
          return {
            theme: 'modern',
            height: 300
          }
        }
      },
      url: {
        default: '',
        type: String
      },
      accept: {
        default: 'image/jpeg, image/png',
        type: String
      },
      maxSize: {
        default: 2097152,
        type: Number
      },
      withCredentials: {
        default: false,
        type: Boolean
      }
    },
    mounted () {
      this.init()
    },
    beforeDestroy () {
      // 销毁tinymce
      this.$emit('on-destroy')
      window.tinymce.remove(`$#{this.Id}`)
    },
    methods: {
       init () {
        const self = this
        
        this.Editor = window.tinymce.init({
          // 默认配置
          ...this.DefaultConfig,
          
          // 图片上传
          images_upload_handler: function (blobInfo, success, failure) {
            if (blobInfo.blob().size > self.maxSize) {
              failure('文件体积过大')
            }
            
            if (self.accept.indexOf(blobInfo.blob().type) >= 0) {
              uploadPic()
            } else {
              failure('图片格式错误')
            }
            function uploadPic () {
              const xhr = new XMLHttpRequest()
              const formData = new FormData()
              xhr.withCredentials = self.withCredentials
              xhr.open('POST', self.url)
              xhr.onload = function () {

                if (xhr.status !== 200) {
                  // 抛出 'on-upload-fail' 钩子
                  self.$emit('on-upload-fail')
                  failure('上传失败: ' + xhr.status)
                  return
                }

                const json = JSON.parse(xhr.responseText)
                // 抛出 'on-upload-complete' 钩子
                self.$emit('on-upload-complete' , [
                  json, success, failure
                ])
              }
              formData.append('file', blobInfo.blob())
              xhr.send(formData)
            }
          },

          // prop内传入的的config
          ...this.config, 
          
          // 挂载的DOM对象
          selector: `#${this.Id}`,
          setup: (editor) => {
            // 抛出 'on-ready' 事件钩子
            editor.on(
              'init', () => {
                self.loading = false
                self.$emit('on-ready')
                editor.setContent(self.value)
              }
            )
            // 抛出 'input' 事件钩子,同步value数据
            editor.on(
              'input change undo redo', () => {
                self.$emit('input', editor.getContent())
              }
            )
          }
        })
      }
    }
  }
</script>

直接引入组件调用就行了

<template>
  <mce-editor 
    :config           = "Config"
     v-model          = "Value"
    :url              = "Url"
    :max-size         = "MaxSize"
    :accept           = "Accept"
    :with-credentials = false
    @on-ready         = "onEditorReady"
    @on-destroy       = "onEditorDestroy"
    @on-upload-success= "onEditorUploadComplete"
    @on-upload-fail   = "onEditorUploadFail"
  ></mce-editor>
</template>

但是作为一名优秀的程序员,这怎么可能够嘛。
下面说下打包的事情

塞入webpack

为了加快页面载入速度就要首先解决载入文件过多的问题,而大部分时间用户并不需要每次打开页面都先加载一遍editor的核心文件,而editor本身也要按需加载内容,一开始想把每个plugin都搞成独立组件模块按需载入,但是这就要涉及到修改编辑器本身源码,或者说对window.tinymce删掉点特性,这些都太麻烦也都有风险,对后面的代码维护影响也大,索性就都先留着。
后面边做边改吧

还是以vue-cli为例
把官网下载的包塞到stataic文件夹中
然后删掉index.html模版中的cdn代码吧不需要了
当然这里有俩选择
要么做成一个异步组件,单独打包,按需载入
要么直接引入到main.js中将包打成为一个巨无霸
所以我选择前者,

首先老规矩 引入编辑器主体

import '../../static/tinymce/tinymce.min.js'

然后刷新下页面,不出意外应该是报这么个错Uncaught SyntaxError: Unexpected token <
眼尖的朋友应该知道是怎么回事了theme.js:1
在默认配置下, tinymce载入的theme的路径居然是这个
Request URL:http://localhost:8080/themes/modern/theme.js
然后我跑去官网搜了下api 只搜到一个叫document_base_url的api,但是根据多年程序员的直觉经验告诉我 不是这货(嗯,我在这里卡住了),网上翻了下各地文献,都没有啊,
那怎么办呢
于是我就跑去看源码...但是4万行...算了...
然后我就在控台打印了下tinymce对象,然后发现了一个叫baseURLstring对象,嗯,有希望了。
在源码里搜了下baseURL
蹦出来这段代码 .... 算了有很多段...
大致思想就是通过当前URI拆出来个baseURL,改掉就行了

window.tinymce.baseURL = '/static/tinymce'

如果需要载入的地址是另一个比如自己公司的cdn的路径,那改成全路径就行了

window.tinymce.baseURL = 'http://cdn.xxx.com/static/tinymce'

貌似路径的问题解决了

但是新的问题又出现了,
插件下过来都是带min的,但默认载入的插件都是不带min的,一定是我源码没看仔细,
然后我又搜了一下代码

if (!baseURL && document.currentScript) {
  src = document.currentScript.src;
  if (src.indexOf('.min') != -1) {
    suffix = '.min';
  }

  baseURL = src.substring(0, src.lastIndexOf('/'));
}

希望就在眼前,貌似是业务我载入的方式是直接导入到模块的,于是一个叫suffix的默认值为空了,于是我去又加了行代码:

window.tinymce.suffix = '.min'

成功!
你看嘛,超级简单的是不是,根本不用改源码,网上说的动不动就去改源码什么的不要信啊不要信,大部分面向对象的事情改个默认值就行了。

对了,还记得前面的语言包嘛,
下过来塞到/static/tinymce/langs文件夹里
然后删掉

import './zh_CN.js'

这行代码
DefaultConfig中放入一个新配置项

language: 'zh_CN'

好了,后面就是模块打包的事情了,

打包

前面打的包有一个问题是默认配置是载入tinyMce本体,那么就会造成这个包大概有500k的体积,如果这个组件不做异步载入的处理,那么对于某些业务来说就是灾难。虽然这么做打开只用载入一个文件,业务比较稳定。
但我觉得这样不优雅所以最后还是把它单独拎出来了。
同理,根据这个库本身的特性,我们完全可以把这么多个必须的plugin按需要直接统一打成一个包,直接载入。这样,我们就又多了一个几百k的plugins包。
然后把plugins包和tinyMce主体包在不阻塞页面加载的情况下,做个懒加载提前缓存好文件方便后面使用,而组件本身在挂载前做个监听window.tinymce全局变量的方法,然后cdn控制下文件的过期时间即可。
这样,在保证了灵活度的前提下也保证了业务载入的速度。

完,感谢阅读。

查看原文

赞 107 收藏 70 评论 121

zhouciming 收藏了文章 · 2019-11-15

vue项目移植tinymce踩坑

2019-2-18

貌似这篇文章帮了大家一些小忙
最近tinymce出5.0版本了,下面的api还是4.x的,新版本可能会有些不适用了,最近业务繁忙,等哪天周末有时间的话我再做点更新 :)


前言

最近因业务需求在项目中嵌入了tinymce这个编辑器,用于满足平台给用户编辑各类新闻内容什么的业务需求,前后也花了不少时间体验和对比了市面上各类开源编辑器。

各大WYSIWYG编辑器的简单比较

UEditor: 因为已经不再维护了,需要大量修改源码,很多都是专门为jsp等服务器渲染项目写的代码需要删除, 然后越删越害怕越删越不敢用,依赖jquery,需要专门用js去parse编辑完成的内容,parse完的内容还可能污染全局css,兼容老浏览器还不错, 但是,我们不怎么考虑兼容IE。所以,告辞。

wangEditor: 中文文档,上手快,依赖jquery,功能少点要花时间去写插件,需要单独为图片上传功能写个接口,老项目忙着上线临时用过,感觉并不适合当前业务这么重的编辑功能于是放弃了。

Quill:api友好, 功能少,需要特定的css去解析文本(这点我不大喜欢),ui好看,适合作为论坛回帖功能使用。

CKEditor: CKEditor目前主流的还是4.x的版本,但是文档看着很瞎眼实在是提不起兴致去配置,草草用了下就放弃了,5.x版本刚从beta结束,需要指定专门的node以及npm版本,虽然功能强大配置灵活ui漂亮不过目前糟糕的兼容性基本是不可能出现在大众视野了。

KingEditor: 丑,不喜欢,不爱用

Draft-js: 知乎最近刚改的文本编辑器就是在draft的基础上开发的,依赖react, 弃。

Medium-editor: 虽然看着感觉很酷炫,但是,不适合我们的业务场景啊, api也简陋可怕。

trix: 嗯,又一个小而美,放弃

Slatereact,放弃

Bootstrap-wysiwygbootstrap, jquery, 放弃

tinymce: 文档好,功能强,bug少,无外部依赖,大家用了都说好,嗯,没错就是它了。

编辑器配置方面只要能看得懂英文耍起来还是比较简单的,适配中碰到的大部分问题都可以通过看文档解决,即便看文档解决不了网上也有大量的文章能告诉你怎么配置能解决。

当然了,主要是我这里需要解决一些别人觉得超简单自己一想都很烦人的需求,比如:

  1. word文档粘贴进来要带格式
  2. 兼容移动端
  3. word文档粘贴进来要正常显示并且还要兼容移动端
  4. 电脑网页里粘贴进来内容要正常显示并且排版还不能乱
  5. 电脑网页拷过来的内容还要兼容到移动端

初始化

因为tinymce的Plugins是按需加载的
为了能先快速上手这个编辑器
就先在vue-cli的index.html中默认塞入一条在线cdn地址

<script data-original="https://cdn.bootcss.com/tinymce/4.7.4/tinymce.min.js"></script>

记得去下载语言包到本地,
然后就在文件内引入

import './zh_CN.js'

后面有机会再写下单独打包的事项,毕竟这货体积还不小。

插入vue组件模板

<template>
  <div>
    <textarea :id= "Id"></textarea>
  </div>
</template>

记得一定要在textarea外面包一层div,不然...你自己试试看就知道了。

组件基础配置

将tinymce通过指定的selector挂载到组件中

<template>
  <div>
    <textarea :id= "Id"></textarea>
  </div>
</template>
<script>
  import './zh_CN.js'
  export default {
    data () {
      const Id = Date.now()
      return {
        Id: Id,
        Editor: null,
        DefaultConfig: {}
      }
    },
    props: {
      value: {
        default: '',
        type: String
      },
      config: {
        type: Object,
        default: () => {
          return {
            theme: 'modern',
            height: 300
          }
        }
      }
    },
    mounted () {
      this.init()
    },
    beforeDestroy () {
      // 销毁tinymce
      this.$emit('on-destroy')
      window.tinymce.remove(`#${this.Id}`)
    },
    methods: {
       init () {
        const self = this
        this.Editor = window.tinymce.init({
          // 默认配置
          ...this.DefaultConfig,
          
          // prop内传入的的config
          ...this.config, 
          
          // 挂载的DOM对象
          selector: `#${this.Id}`,
          
          setup: (editor) => {
            // 抛出 'on-ready' 事件钩子
            editor.on(
              'init', () => {
                self.loading = false
                self.$emit('on-ready')
                editor.setContent(self.value)
              }
            )
            // 抛出 'input' 事件钩子,同步value数据
            editor.on(
              'input change undo redo', () => {
                self.$emit('input', editor.getContent())
              }
            )
          }
        })
      }
    }
  }
</script>

好了,组件基本的初始化完成,后面正式开始踩坑之旅

API

具体内容看官网的API就行,英语不好的用chrome翻译下对照着demo也能看个七七八八,当然主要原因还是我比较懒。

我这边根据自身业务需求在组件的data内写了个默认配置


DefaultConfig: {
  // GLOBAL
  height: 500,
  theme: 'modern',
  menubar: false,
  toolbar: `styleselect | fontselect | formatselect | fontsizeselect | forecolor backcolor | bold italic underline strikethrough | image  media | table | alignleft aligncenter alignright alignjustify | outdent indent | numlist bullist | preview removeformat  hr | paste code  link | undo redo | fullscreen `,
  plugins: `
    paste
    importcss
    image
    code
    table
    advlist
    fullscreen
    link
    media
    lists
    textcolor
    colorpicker
    hr
    preview
  `,

  
  // CONFIG

  forced_root_block: 'p',
  force_p_newlines: true,
  importcss_append: true,

 // CONFIG: ContentStyle 这块很重要, 在最后呈现的页面也要写入这个基本样式保证前后一致, `table`和`img`的问题基本就靠这个来填坑了
  content_style: `
    *                         { padding:0; margin:0; }
    html, body                { height:100%; }
    img                       { max-width:100%; display:block;height:auto; }
    a                         { text-decoration: none; }
    iframe                    { width: 100%; }
    p                         { line-height:1.6; margin: 0px; }
    table                     { word-wrap:break-word; word-break:break-all; max-width:100%; border:none; border-color:#999; }
    .mce-object-iframe        { width:100%; box-sizing:border-box; margin:0; padding:0; }
    ul,ol                     { list-style-position:inside; }
  `,

  insert_button_items: 'image link | inserttable',

  // CONFIG: Paste
  paste_retain_style_properties: 'all',
  paste_word_valid_elements: '*[*]',        // word需要它
  paste_data_images: true,                  // 粘贴的同时能把内容里的图片自动上传,非常强力的功能
  paste_convert_word_fake_lists: false,     // 插入word文档需要该属性
  paste_webkit_styles: 'all',
  paste_merge_formats: true,
  nonbreaking_force_tab: false,
  paste_auto_cleanup_on_paste: false,

  // CONFIG: Font
  fontsize_formats: '10px 11px 12px 14px 16px 18px 20px 24px',

  // CONFIG: StyleSelect
  style_formats: [
    {
      title: '首行缩进',
      block: 'p',
      styles: { 'text-indent': '2em' }
    },
    {
      title: '行高',
      items: [
        {title: '1', styles: { 'line-height': '1' }, inline: 'span'},
        {title: '1.5', styles: { 'line-height': '1.5' }, inline: 'span'},
        {title: '2', styles: { 'line-height': '2' }, inline: 'span'},
        {title: '2.5', styles: { 'line-height': '2.5' }, inline: 'span'},
        {title: '3', styles: { 'line-height': '3' }, inline: 'span'}
      ]
    }
  ],

  // FontSelect
  font_formats: `
    微软雅黑=微软雅黑;
    宋体=宋体;
    黑体=黑体;
    仿宋=仿宋;
    楷体=楷体;
    隶书=隶书;
    幼圆=幼圆;
    Andale Mono=andale mono,times;
    Arial=arial, helvetica,
    sans-serif;
    Arial Black=arial black, avant garde;
    Book Antiqua=book antiqua,palatino;
    Comic Sans MS=comic sans ms,sans-serif;
    Courier New=courier new,courier;
    Georgia=georgia,palatino;
    Helvetica=helvetica;
    Impact=impact,chicago;
    Symbol=symbol;
    Tahoma=tahoma,arial,helvetica,sans-serif;
    Terminal=terminal,monaco;
    Times New Roman=times new roman,times;
    Trebuchet MS=trebuchet ms,geneva;
    Verdana=verdana,geneva;
    Webdings=webdings;
    Wingdings=wingdings,zapf dingbats`,

  // Tab
  tabfocus_elements: ':prev,:next',
  object_resizing: true,

  // Image
  imagetools_toolbar: 'rotateleft rotateright | flipv fliph | editimage imageoptions'
}

因为本人比较懒,以上配置导出的代码可能会有代码注入的风险,建议保存的时候再前后端都做下注入过滤,不过一般数据安全问题主要还是服务器那边的事情?。

后面的图片上传可以单独拆出来做个小配置,直接写到props里好了。

  url: {
    default: '',
    type: String
  },
  accept: {
    default: 'image/jpeg, image/png',
    type: String
  },
  maxSize: {
    default: 2097152,
    type: Number
  },
  withCredentials: {
    default: false,
    type: Boolean
  }

然后把这套东西塞到init配置里


  // 图片上传
  images_upload_handler: function (blobInfo, success, failure) {
    if (blobInfo.blob().size > self.maxSize) {
      failure('文件体积过大')
    }
    
    if (self.accept.indexOf(blobInfo.blob().type) >= 0) {
      uploadPic()
    } else {
      failure('图片格式错误')
    }
    function uploadPic () {
      const xhr = new XMLHttpRequest()
      const formData = new FormData()
      xhr.withCredentials = self.withCredentials
      xhr.open('POST', self.url)
      xhr.onload = function () {

        if (xhr.status !== 200) {
          // 抛出 'on-upload-fail' 钩子
          self.$emit('on-upload-fail')
          failure('上传失败: ' + xhr.status)
          return
        }

        const json = JSON.parse(xhr.responseText)
        // 抛出 'on-upload-success' 钩子
        self.$emit('on-upload-complete' , [
          json, success, failure
        ])
      }
      formData.append('file', blobInfo.blob())
      xhr.send(formData)
    }
  }

至此, 一个组件的封装基本算是完成了

看下初阶成果

<template>
  <div>
    <textarea :id= "Id"></textarea>
  </div>
</template>
<script>
  import './zh_CN.js'
  export default {
    data () {
      const Id = Date.now()
      return {
        Id: Id,
        Editor: null,
        DefaultConfig: {
          // GLOBAL
          height: 500,
          theme: 'modern',
          menubar: false,
          toolbar: `styleselect | fontselect | formatselect | fontsizeselect | forecolor backcolor | bold italic underline strikethrough | image  media | table | alignleft aligncenter alignright alignjustify | outdent indent | numlist bullist | preview removeformat  hr | paste code  link | undo redo | fullscreen `,
          plugins: `
            paste
            importcss
            image
            code
            table
            advlist
            fullscreen
            link
            media
            lists
            textcolor
            colorpicker
            hr
            preview
          `,

          
          // CONFIG

          forced_root_block: 'p',
          force_p_newlines: true,
          importcss_append: true,

        // CONFIG: ContentStyle 这块很重要, 在最后呈现的页面也要写入这个基本样式保证前后一致, `table`和`img`的问题基本就靠这个来填坑了
          content_style: `
            *                         { padding:0; margin:0; }
            html, body                { height:100%; }
            img                       { max-width:100%; display:block;height:auto; }
            a                         { text-decoration: none; }
            iframe                    { width: 100%; }
            p                         { line-height:1.6; margin: 0px; }
            table                     { word-wrap:break-word; word-break:break-all; max-width:100%; border:none; border-color:#999; }
            .mce-object-iframe        { width:100%; box-sizing:border-box; margin:0; padding:0; }
            ul,ol                     { list-style-position:inside; }
          `,

          insert_button_items: 'image link | inserttable',

          // CONFIG: Paste
          paste_retain_style_properties: 'all',
          paste_word_valid_elements: '*[*]',        // word需要它
          paste_data_images: true,                  // 粘贴的同时能把内容里的图片自动上传,非常强力的功能
          paste_convert_word_fake_lists: false,     // 插入word文档需要该属性
          paste_webkit_styles: 'all',
          paste_merge_formats: true,
          nonbreaking_force_tab: false,
          paste_auto_cleanup_on_paste: false,

          // CONFIG: Font
          fontsize_formats: '10px 11px 12px 14px 16px 18px 20px 24px',

          // CONFIG: StyleSelect
          style_formats: [
            {
              title: '首行缩进',
              block: 'p',
              styles: { 'text-indent': '2em' }
            },
            {
              title: '行高',
              items: [
                {title: '1', styles: { 'line-height': '1' }, inline: 'span'},
                {title: '1.5', styles: { 'line-height': '1.5' }, inline: 'span'},
                {title: '2', styles: { 'line-height': '2' }, inline: 'span'},
                {title: '2.5', styles: { 'line-height': '2.5' }, inline: 'span'},
                {title: '3', styles: { 'line-height': '3' }, inline: 'span'}
              ]
            }
          ],

          // FontSelect
          font_formats: `
            微软雅黑=微软雅黑;
            宋体=宋体;
            黑体=黑体;
            仿宋=仿宋;
            楷体=楷体;
            隶书=隶书;
            幼圆=幼圆;
            Andale Mono=andale mono,times;
            Arial=arial, helvetica,
            sans-serif;
            Arial Black=arial black, avant garde;
            Book Antiqua=book antiqua,palatino;
            Comic Sans MS=comic sans ms,sans-serif;
            Courier New=courier new,courier;
            Georgia=georgia,palatino;
            Helvetica=helvetica;
            Impact=impact,chicago;
            Symbol=symbol;
            Tahoma=tahoma,arial,helvetica,sans-serif;
            Terminal=terminal,monaco;
            Times New Roman=times new roman,times;
            Trebuchet MS=trebuchet ms,geneva;
            Verdana=verdana,geneva;
            Webdings=webdings;
            Wingdings=wingdings,zapf dingbats`,

          // Tab
          tabfocus_elements: ':prev,:next',
          object_resizing: true,

          // Image
          imagetools_toolbar: 'rotateleft rotateright | flipv fliph | editimage imageoptions'
        }
      }
    },
    props: {
      value: {
        default: '',
        type: String
      },
      config: {
        type: Object,
        default: () => {
          return {
            theme: 'modern',
            height: 300
          }
        }
      },
      url: {
        default: '',
        type: String
      },
      accept: {
        default: 'image/jpeg, image/png',
        type: String
      },
      maxSize: {
        default: 2097152,
        type: Number
      },
      withCredentials: {
        default: false,
        type: Boolean
      }
    },
    mounted () {
      this.init()
    },
    beforeDestroy () {
      // 销毁tinymce
      this.$emit('on-destroy')
      window.tinymce.remove(`$#{this.Id}`)
    },
    methods: {
       init () {
        const self = this
        
        this.Editor = window.tinymce.init({
          // 默认配置
          ...this.DefaultConfig,
          
          // 图片上传
          images_upload_handler: function (blobInfo, success, failure) {
            if (blobInfo.blob().size > self.maxSize) {
              failure('文件体积过大')
            }
            
            if (self.accept.indexOf(blobInfo.blob().type) >= 0) {
              uploadPic()
            } else {
              failure('图片格式错误')
            }
            function uploadPic () {
              const xhr = new XMLHttpRequest()
              const formData = new FormData()
              xhr.withCredentials = self.withCredentials
              xhr.open('POST', self.url)
              xhr.onload = function () {

                if (xhr.status !== 200) {
                  // 抛出 'on-upload-fail' 钩子
                  self.$emit('on-upload-fail')
                  failure('上传失败: ' + xhr.status)
                  return
                }

                const json = JSON.parse(xhr.responseText)
                // 抛出 'on-upload-complete' 钩子
                self.$emit('on-upload-complete' , [
                  json, success, failure
                ])
              }
              formData.append('file', blobInfo.blob())
              xhr.send(formData)
            }
          },

          // prop内传入的的config
          ...this.config, 
          
          // 挂载的DOM对象
          selector: `#${this.Id}`,
          setup: (editor) => {
            // 抛出 'on-ready' 事件钩子
            editor.on(
              'init', () => {
                self.loading = false
                self.$emit('on-ready')
                editor.setContent(self.value)
              }
            )
            // 抛出 'input' 事件钩子,同步value数据
            editor.on(
              'input change undo redo', () => {
                self.$emit('input', editor.getContent())
              }
            )
          }
        })
      }
    }
  }
</script>

直接引入组件调用就行了

<template>
  <mce-editor 
    :config           = "Config"
     v-model          = "Value"
    :url              = "Url"
    :max-size         = "MaxSize"
    :accept           = "Accept"
    :with-credentials = false
    @on-ready         = "onEditorReady"
    @on-destroy       = "onEditorDestroy"
    @on-upload-success= "onEditorUploadComplete"
    @on-upload-fail   = "onEditorUploadFail"
  ></mce-editor>
</template>

但是作为一名优秀的程序员,这怎么可能够嘛。
下面说下打包的事情

塞入webpack

为了加快页面载入速度就要首先解决载入文件过多的问题,而大部分时间用户并不需要每次打开页面都先加载一遍editor的核心文件,而editor本身也要按需加载内容,一开始想把每个plugin都搞成独立组件模块按需载入,但是这就要涉及到修改编辑器本身源码,或者说对window.tinymce删掉点特性,这些都太麻烦也都有风险,对后面的代码维护影响也大,索性就都先留着。
后面边做边改吧

还是以vue-cli为例
把官网下载的包塞到stataic文件夹中
然后删掉index.html模版中的cdn代码吧不需要了
当然这里有俩选择
要么做成一个异步组件,单独打包,按需载入
要么直接引入到main.js中将包打成为一个巨无霸
所以我选择前者,

首先老规矩 引入编辑器主体

import '../../static/tinymce/tinymce.min.js'

然后刷新下页面,不出意外应该是报这么个错Uncaught SyntaxError: Unexpected token <
眼尖的朋友应该知道是怎么回事了theme.js:1
在默认配置下, tinymce载入的theme的路径居然是这个
Request URL:http://localhost:8080/themes/modern/theme.js
然后我跑去官网搜了下api 只搜到一个叫document_base_url的api,但是根据多年程序员的直觉经验告诉我 不是这货(嗯,我在这里卡住了),网上翻了下各地文献,都没有啊,
那怎么办呢
于是我就跑去看源码...但是4万行...算了...
然后我就在控台打印了下tinymce对象,然后发现了一个叫baseURLstring对象,嗯,有希望了。
在源码里搜了下baseURL
蹦出来这段代码 .... 算了有很多段...
大致思想就是通过当前URI拆出来个baseURL,改掉就行了

window.tinymce.baseURL = '/static/tinymce'

如果需要载入的地址是另一个比如自己公司的cdn的路径,那改成全路径就行了

window.tinymce.baseURL = 'http://cdn.xxx.com/static/tinymce'

貌似路径的问题解决了

但是新的问题又出现了,
插件下过来都是带min的,但默认载入的插件都是不带min的,一定是我源码没看仔细,
然后我又搜了一下代码

if (!baseURL && document.currentScript) {
  src = document.currentScript.src;
  if (src.indexOf('.min') != -1) {
    suffix = '.min';
  }

  baseURL = src.substring(0, src.lastIndexOf('/'));
}

希望就在眼前,貌似是业务我载入的方式是直接导入到模块的,于是一个叫suffix的默认值为空了,于是我去又加了行代码:

window.tinymce.suffix = '.min'

成功!
你看嘛,超级简单的是不是,根本不用改源码,网上说的动不动就去改源码什么的不要信啊不要信,大部分面向对象的事情改个默认值就行了。

对了,还记得前面的语言包嘛,
下过来塞到/static/tinymce/langs文件夹里
然后删掉

import './zh_CN.js'

这行代码
DefaultConfig中放入一个新配置项

language: 'zh_CN'

好了,后面就是模块打包的事情了,

打包

前面打的包有一个问题是默认配置是载入tinyMce本体,那么就会造成这个包大概有500k的体积,如果这个组件不做异步载入的处理,那么对于某些业务来说就是灾难。虽然这么做打开只用载入一个文件,业务比较稳定。
但我觉得这样不优雅所以最后还是把它单独拎出来了。
同理,根据这个库本身的特性,我们完全可以把这么多个必须的plugin按需要直接统一打成一个包,直接载入。这样,我们就又多了一个几百k的plugins包。
然后把plugins包和tinyMce主体包在不阻塞页面加载的情况下,做个懒加载提前缓存好文件方便后面使用,而组件本身在挂载前做个监听window.tinymce全局变量的方法,然后cdn控制下文件的过期时间即可。
这样,在保证了灵活度的前提下也保证了业务载入的速度。

完,感谢阅读。

查看原文

zhouciming 收藏了文章 · 2019-11-15

vue项目移植tinymce踩坑

2019-2-18

貌似这篇文章帮了大家一些小忙
最近tinymce出5.0版本了,下面的api还是4.x的,新版本可能会有些不适用了,最近业务繁忙,等哪天周末有时间的话我再做点更新 :)


前言

最近因业务需求在项目中嵌入了tinymce这个编辑器,用于满足平台给用户编辑各类新闻内容什么的业务需求,前后也花了不少时间体验和对比了市面上各类开源编辑器。

各大WYSIWYG编辑器的简单比较

UEditor: 因为已经不再维护了,需要大量修改源码,很多都是专门为jsp等服务器渲染项目写的代码需要删除, 然后越删越害怕越删越不敢用,依赖jquery,需要专门用js去parse编辑完成的内容,parse完的内容还可能污染全局css,兼容老浏览器还不错, 但是,我们不怎么考虑兼容IE。所以,告辞。

wangEditor: 中文文档,上手快,依赖jquery,功能少点要花时间去写插件,需要单独为图片上传功能写个接口,老项目忙着上线临时用过,感觉并不适合当前业务这么重的编辑功能于是放弃了。

Quill:api友好, 功能少,需要特定的css去解析文本(这点我不大喜欢),ui好看,适合作为论坛回帖功能使用。

CKEditor: CKEditor目前主流的还是4.x的版本,但是文档看着很瞎眼实在是提不起兴致去配置,草草用了下就放弃了,5.x版本刚从beta结束,需要指定专门的node以及npm版本,虽然功能强大配置灵活ui漂亮不过目前糟糕的兼容性基本是不可能出现在大众视野了。

KingEditor: 丑,不喜欢,不爱用

Draft-js: 知乎最近刚改的文本编辑器就是在draft的基础上开发的,依赖react, 弃。

Medium-editor: 虽然看着感觉很酷炫,但是,不适合我们的业务场景啊, api也简陋可怕。

trix: 嗯,又一个小而美,放弃

Slatereact,放弃

Bootstrap-wysiwygbootstrap, jquery, 放弃

tinymce: 文档好,功能强,bug少,无外部依赖,大家用了都说好,嗯,没错就是它了。

编辑器配置方面只要能看得懂英文耍起来还是比较简单的,适配中碰到的大部分问题都可以通过看文档解决,即便看文档解决不了网上也有大量的文章能告诉你怎么配置能解决。

当然了,主要是我这里需要解决一些别人觉得超简单自己一想都很烦人的需求,比如:

  1. word文档粘贴进来要带格式
  2. 兼容移动端
  3. word文档粘贴进来要正常显示并且还要兼容移动端
  4. 电脑网页里粘贴进来内容要正常显示并且排版还不能乱
  5. 电脑网页拷过来的内容还要兼容到移动端

初始化

因为tinymce的Plugins是按需加载的
为了能先快速上手这个编辑器
就先在vue-cli的index.html中默认塞入一条在线cdn地址

<script data-original="https://cdn.bootcss.com/tinymce/4.7.4/tinymce.min.js"></script>

记得去下载语言包到本地,
然后就在文件内引入

import './zh_CN.js'

后面有机会再写下单独打包的事项,毕竟这货体积还不小。

插入vue组件模板

<template>
  <div>
    <textarea :id= "Id"></textarea>
  </div>
</template>

记得一定要在textarea外面包一层div,不然...你自己试试看就知道了。

组件基础配置

将tinymce通过指定的selector挂载到组件中

<template>
  <div>
    <textarea :id= "Id"></textarea>
  </div>
</template>
<script>
  import './zh_CN.js'
  export default {
    data () {
      const Id = Date.now()
      return {
        Id: Id,
        Editor: null,
        DefaultConfig: {}
      }
    },
    props: {
      value: {
        default: '',
        type: String
      },
      config: {
        type: Object,
        default: () => {
          return {
            theme: 'modern',
            height: 300
          }
        }
      }
    },
    mounted () {
      this.init()
    },
    beforeDestroy () {
      // 销毁tinymce
      this.$emit('on-destroy')
      window.tinymce.remove(`#${this.Id}`)
    },
    methods: {
       init () {
        const self = this
        this.Editor = window.tinymce.init({
          // 默认配置
          ...this.DefaultConfig,
          
          // prop内传入的的config
          ...this.config, 
          
          // 挂载的DOM对象
          selector: `#${this.Id}`,
          
          setup: (editor) => {
            // 抛出 'on-ready' 事件钩子
            editor.on(
              'init', () => {
                self.loading = false
                self.$emit('on-ready')
                editor.setContent(self.value)
              }
            )
            // 抛出 'input' 事件钩子,同步value数据
            editor.on(
              'input change undo redo', () => {
                self.$emit('input', editor.getContent())
              }
            )
          }
        })
      }
    }
  }
</script>

好了,组件基本的初始化完成,后面正式开始踩坑之旅

API

具体内容看官网的API就行,英语不好的用chrome翻译下对照着demo也能看个七七八八,当然主要原因还是我比较懒。

我这边根据自身业务需求在组件的data内写了个默认配置


DefaultConfig: {
  // GLOBAL
  height: 500,
  theme: 'modern',
  menubar: false,
  toolbar: `styleselect | fontselect | formatselect | fontsizeselect | forecolor backcolor | bold italic underline strikethrough | image  media | table | alignleft aligncenter alignright alignjustify | outdent indent | numlist bullist | preview removeformat  hr | paste code  link | undo redo | fullscreen `,
  plugins: `
    paste
    importcss
    image
    code
    table
    advlist
    fullscreen
    link
    media
    lists
    textcolor
    colorpicker
    hr
    preview
  `,

  
  // CONFIG

  forced_root_block: 'p',
  force_p_newlines: true,
  importcss_append: true,

 // CONFIG: ContentStyle 这块很重要, 在最后呈现的页面也要写入这个基本样式保证前后一致, `table`和`img`的问题基本就靠这个来填坑了
  content_style: `
    *                         { padding:0; margin:0; }
    html, body                { height:100%; }
    img                       { max-width:100%; display:block;height:auto; }
    a                         { text-decoration: none; }
    iframe                    { width: 100%; }
    p                         { line-height:1.6; margin: 0px; }
    table                     { word-wrap:break-word; word-break:break-all; max-width:100%; border:none; border-color:#999; }
    .mce-object-iframe        { width:100%; box-sizing:border-box; margin:0; padding:0; }
    ul,ol                     { list-style-position:inside; }
  `,

  insert_button_items: 'image link | inserttable',

  // CONFIG: Paste
  paste_retain_style_properties: 'all',
  paste_word_valid_elements: '*[*]',        // word需要它
  paste_data_images: true,                  // 粘贴的同时能把内容里的图片自动上传,非常强力的功能
  paste_convert_word_fake_lists: false,     // 插入word文档需要该属性
  paste_webkit_styles: 'all',
  paste_merge_formats: true,
  nonbreaking_force_tab: false,
  paste_auto_cleanup_on_paste: false,

  // CONFIG: Font
  fontsize_formats: '10px 11px 12px 14px 16px 18px 20px 24px',

  // CONFIG: StyleSelect
  style_formats: [
    {
      title: '首行缩进',
      block: 'p',
      styles: { 'text-indent': '2em' }
    },
    {
      title: '行高',
      items: [
        {title: '1', styles: { 'line-height': '1' }, inline: 'span'},
        {title: '1.5', styles: { 'line-height': '1.5' }, inline: 'span'},
        {title: '2', styles: { 'line-height': '2' }, inline: 'span'},
        {title: '2.5', styles: { 'line-height': '2.5' }, inline: 'span'},
        {title: '3', styles: { 'line-height': '3' }, inline: 'span'}
      ]
    }
  ],

  // FontSelect
  font_formats: `
    微软雅黑=微软雅黑;
    宋体=宋体;
    黑体=黑体;
    仿宋=仿宋;
    楷体=楷体;
    隶书=隶书;
    幼圆=幼圆;
    Andale Mono=andale mono,times;
    Arial=arial, helvetica,
    sans-serif;
    Arial Black=arial black, avant garde;
    Book Antiqua=book antiqua,palatino;
    Comic Sans MS=comic sans ms,sans-serif;
    Courier New=courier new,courier;
    Georgia=georgia,palatino;
    Helvetica=helvetica;
    Impact=impact,chicago;
    Symbol=symbol;
    Tahoma=tahoma,arial,helvetica,sans-serif;
    Terminal=terminal,monaco;
    Times New Roman=times new roman,times;
    Trebuchet MS=trebuchet ms,geneva;
    Verdana=verdana,geneva;
    Webdings=webdings;
    Wingdings=wingdings,zapf dingbats`,

  // Tab
  tabfocus_elements: ':prev,:next',
  object_resizing: true,

  // Image
  imagetools_toolbar: 'rotateleft rotateright | flipv fliph | editimage imageoptions'
}

因为本人比较懒,以上配置导出的代码可能会有代码注入的风险,建议保存的时候再前后端都做下注入过滤,不过一般数据安全问题主要还是服务器那边的事情?。

后面的图片上传可以单独拆出来做个小配置,直接写到props里好了。

  url: {
    default: '',
    type: String
  },
  accept: {
    default: 'image/jpeg, image/png',
    type: String
  },
  maxSize: {
    default: 2097152,
    type: Number
  },
  withCredentials: {
    default: false,
    type: Boolean
  }

然后把这套东西塞到init配置里


  // 图片上传
  images_upload_handler: function (blobInfo, success, failure) {
    if (blobInfo.blob().size > self.maxSize) {
      failure('文件体积过大')
    }
    
    if (self.accept.indexOf(blobInfo.blob().type) >= 0) {
      uploadPic()
    } else {
      failure('图片格式错误')
    }
    function uploadPic () {
      const xhr = new XMLHttpRequest()
      const formData = new FormData()
      xhr.withCredentials = self.withCredentials
      xhr.open('POST', self.url)
      xhr.onload = function () {

        if (xhr.status !== 200) {
          // 抛出 'on-upload-fail' 钩子
          self.$emit('on-upload-fail')
          failure('上传失败: ' + xhr.status)
          return
        }

        const json = JSON.parse(xhr.responseText)
        // 抛出 'on-upload-success' 钩子
        self.$emit('on-upload-complete' , [
          json, success, failure
        ])
      }
      formData.append('file', blobInfo.blob())
      xhr.send(formData)
    }
  }

至此, 一个组件的封装基本算是完成了

看下初阶成果

<template>
  <div>
    <textarea :id= "Id"></textarea>
  </div>
</template>
<script>
  import './zh_CN.js'
  export default {
    data () {
      const Id = Date.now()
      return {
        Id: Id,
        Editor: null,
        DefaultConfig: {
          // GLOBAL
          height: 500,
          theme: 'modern',
          menubar: false,
          toolbar: `styleselect | fontselect | formatselect | fontsizeselect | forecolor backcolor | bold italic underline strikethrough | image  media | table | alignleft aligncenter alignright alignjustify | outdent indent | numlist bullist | preview removeformat  hr | paste code  link | undo redo | fullscreen `,
          plugins: `
            paste
            importcss
            image
            code
            table
            advlist
            fullscreen
            link
            media
            lists
            textcolor
            colorpicker
            hr
            preview
          `,

          
          // CONFIG

          forced_root_block: 'p',
          force_p_newlines: true,
          importcss_append: true,

        // CONFIG: ContentStyle 这块很重要, 在最后呈现的页面也要写入这个基本样式保证前后一致, `table`和`img`的问题基本就靠这个来填坑了
          content_style: `
            *                         { padding:0; margin:0; }
            html, body                { height:100%; }
            img                       { max-width:100%; display:block;height:auto; }
            a                         { text-decoration: none; }
            iframe                    { width: 100%; }
            p                         { line-height:1.6; margin: 0px; }
            table                     { word-wrap:break-word; word-break:break-all; max-width:100%; border:none; border-color:#999; }
            .mce-object-iframe        { width:100%; box-sizing:border-box; margin:0; padding:0; }
            ul,ol                     { list-style-position:inside; }
          `,

          insert_button_items: 'image link | inserttable',

          // CONFIG: Paste
          paste_retain_style_properties: 'all',
          paste_word_valid_elements: '*[*]',        // word需要它
          paste_data_images: true,                  // 粘贴的同时能把内容里的图片自动上传,非常强力的功能
          paste_convert_word_fake_lists: false,     // 插入word文档需要该属性
          paste_webkit_styles: 'all',
          paste_merge_formats: true,
          nonbreaking_force_tab: false,
          paste_auto_cleanup_on_paste: false,

          // CONFIG: Font
          fontsize_formats: '10px 11px 12px 14px 16px 18px 20px 24px',

          // CONFIG: StyleSelect
          style_formats: [
            {
              title: '首行缩进',
              block: 'p',
              styles: { 'text-indent': '2em' }
            },
            {
              title: '行高',
              items: [
                {title: '1', styles: { 'line-height': '1' }, inline: 'span'},
                {title: '1.5', styles: { 'line-height': '1.5' }, inline: 'span'},
                {title: '2', styles: { 'line-height': '2' }, inline: 'span'},
                {title: '2.5', styles: { 'line-height': '2.5' }, inline: 'span'},
                {title: '3', styles: { 'line-height': '3' }, inline: 'span'}
              ]
            }
          ],

          // FontSelect
          font_formats: `
            微软雅黑=微软雅黑;
            宋体=宋体;
            黑体=黑体;
            仿宋=仿宋;
            楷体=楷体;
            隶书=隶书;
            幼圆=幼圆;
            Andale Mono=andale mono,times;
            Arial=arial, helvetica,
            sans-serif;
            Arial Black=arial black, avant garde;
            Book Antiqua=book antiqua,palatino;
            Comic Sans MS=comic sans ms,sans-serif;
            Courier New=courier new,courier;
            Georgia=georgia,palatino;
            Helvetica=helvetica;
            Impact=impact,chicago;
            Symbol=symbol;
            Tahoma=tahoma,arial,helvetica,sans-serif;
            Terminal=terminal,monaco;
            Times New Roman=times new roman,times;
            Trebuchet MS=trebuchet ms,geneva;
            Verdana=verdana,geneva;
            Webdings=webdings;
            Wingdings=wingdings,zapf dingbats`,

          // Tab
          tabfocus_elements: ':prev,:next',
          object_resizing: true,

          // Image
          imagetools_toolbar: 'rotateleft rotateright | flipv fliph | editimage imageoptions'
        }
      }
    },
    props: {
      value: {
        default: '',
        type: String
      },
      config: {
        type: Object,
        default: () => {
          return {
            theme: 'modern',
            height: 300
          }
        }
      },
      url: {
        default: '',
        type: String
      },
      accept: {
        default: 'image/jpeg, image/png',
        type: String
      },
      maxSize: {
        default: 2097152,
        type: Number
      },
      withCredentials: {
        default: false,
        type: Boolean
      }
    },
    mounted () {
      this.init()
    },
    beforeDestroy () {
      // 销毁tinymce
      this.$emit('on-destroy')
      window.tinymce.remove(`$#{this.Id}`)
    },
    methods: {
       init () {
        const self = this
        
        this.Editor = window.tinymce.init({
          // 默认配置
          ...this.DefaultConfig,
          
          // 图片上传
          images_upload_handler: function (blobInfo, success, failure) {
            if (blobInfo.blob().size > self.maxSize) {
              failure('文件体积过大')
            }
            
            if (self.accept.indexOf(blobInfo.blob().type) >= 0) {
              uploadPic()
            } else {
              failure('图片格式错误')
            }
            function uploadPic () {
              const xhr = new XMLHttpRequest()
              const formData = new FormData()
              xhr.withCredentials = self.withCredentials
              xhr.open('POST', self.url)
              xhr.onload = function () {

                if (xhr.status !== 200) {
                  // 抛出 'on-upload-fail' 钩子
                  self.$emit('on-upload-fail')
                  failure('上传失败: ' + xhr.status)
                  return
                }

                const json = JSON.parse(xhr.responseText)
                // 抛出 'on-upload-complete' 钩子
                self.$emit('on-upload-complete' , [
                  json, success, failure
                ])
              }
              formData.append('file', blobInfo.blob())
              xhr.send(formData)
            }
          },

          // prop内传入的的config
          ...this.config, 
          
          // 挂载的DOM对象
          selector: `#${this.Id}`,
          setup: (editor) => {
            // 抛出 'on-ready' 事件钩子
            editor.on(
              'init', () => {
                self.loading = false
                self.$emit('on-ready')
                editor.setContent(self.value)
              }
            )
            // 抛出 'input' 事件钩子,同步value数据
            editor.on(
              'input change undo redo', () => {
                self.$emit('input', editor.getContent())
              }
            )
          }
        })
      }
    }
  }
</script>

直接引入组件调用就行了

<template>
  <mce-editor 
    :config           = "Config"
     v-model          = "Value"
    :url              = "Url"
    :max-size         = "MaxSize"
    :accept           = "Accept"
    :with-credentials = false
    @on-ready         = "onEditorReady"
    @on-destroy       = "onEditorDestroy"
    @on-upload-success= "onEditorUploadComplete"
    @on-upload-fail   = "onEditorUploadFail"
  ></mce-editor>
</template>

但是作为一名优秀的程序员,这怎么可能够嘛。
下面说下打包的事情

塞入webpack

为了加快页面载入速度就要首先解决载入文件过多的问题,而大部分时间用户并不需要每次打开页面都先加载一遍editor的核心文件,而editor本身也要按需加载内容,一开始想把每个plugin都搞成独立组件模块按需载入,但是这就要涉及到修改编辑器本身源码,或者说对window.tinymce删掉点特性,这些都太麻烦也都有风险,对后面的代码维护影响也大,索性就都先留着。
后面边做边改吧

还是以vue-cli为例
把官网下载的包塞到stataic文件夹中
然后删掉index.html模版中的cdn代码吧不需要了
当然这里有俩选择
要么做成一个异步组件,单独打包,按需载入
要么直接引入到main.js中将包打成为一个巨无霸
所以我选择前者,

首先老规矩 引入编辑器主体

import '../../static/tinymce/tinymce.min.js'

然后刷新下页面,不出意外应该是报这么个错Uncaught SyntaxError: Unexpected token <
眼尖的朋友应该知道是怎么回事了theme.js:1
在默认配置下, tinymce载入的theme的路径居然是这个
Request URL:http://localhost:8080/themes/modern/theme.js
然后我跑去官网搜了下api 只搜到一个叫document_base_url的api,但是根据多年程序员的直觉经验告诉我 不是这货(嗯,我在这里卡住了),网上翻了下各地文献,都没有啊,
那怎么办呢
于是我就跑去看源码...但是4万行...算了...
然后我就在控台打印了下tinymce对象,然后发现了一个叫baseURLstring对象,嗯,有希望了。
在源码里搜了下baseURL
蹦出来这段代码 .... 算了有很多段...
大致思想就是通过当前URI拆出来个baseURL,改掉就行了

window.tinymce.baseURL = '/static/tinymce'

如果需要载入的地址是另一个比如自己公司的cdn的路径,那改成全路径就行了

window.tinymce.baseURL = 'http://cdn.xxx.com/static/tinymce'

貌似路径的问题解决了

但是新的问题又出现了,
插件下过来都是带min的,但默认载入的插件都是不带min的,一定是我源码没看仔细,
然后我又搜了一下代码

if (!baseURL && document.currentScript) {
  src = document.currentScript.src;
  if (src.indexOf('.min') != -1) {
    suffix = '.min';
  }

  baseURL = src.substring(0, src.lastIndexOf('/'));
}

希望就在眼前,貌似是业务我载入的方式是直接导入到模块的,于是一个叫suffix的默认值为空了,于是我去又加了行代码:

window.tinymce.suffix = '.min'

成功!
你看嘛,超级简单的是不是,根本不用改源码,网上说的动不动就去改源码什么的不要信啊不要信,大部分面向对象的事情改个默认值就行了。

对了,还记得前面的语言包嘛,
下过来塞到/static/tinymce/langs文件夹里
然后删掉

import './zh_CN.js'

这行代码
DefaultConfig中放入一个新配置项

language: 'zh_CN'

好了,后面就是模块打包的事情了,

打包

前面打的包有一个问题是默认配置是载入tinyMce本体,那么就会造成这个包大概有500k的体积,如果这个组件不做异步载入的处理,那么对于某些业务来说就是灾难。虽然这么做打开只用载入一个文件,业务比较稳定。
但我觉得这样不优雅所以最后还是把它单独拎出来了。
同理,根据这个库本身的特性,我们完全可以把这么多个必须的plugin按需要直接统一打成一个包,直接载入。这样,我们就又多了一个几百k的plugins包。
然后把plugins包和tinyMce主体包在不阻塞页面加载的情况下,做个懒加载提前缓存好文件方便后面使用,而组件本身在挂载前做个监听window.tinymce全局变量的方法,然后cdn控制下文件的过期时间即可。
这样,在保证了灵活度的前提下也保证了业务载入的速度。

完,感谢阅读。

查看原文

zhouciming 收藏了文章 · 2019-09-26

React-Router在单页应用中的简单介绍

在React中使用React Router来构建单页应用

一、为什么需要路由

在我们使用React来构建单页App的时候,最大的区别是,导航一个页面应用程序并不涉及到一个全新的页面。而是你的整个应用是在同一个页面进行操作的。

当你加载网页内容的时候,将会变得有一点困难,困难的部分不是加载内容本身,这相对来说比较容易,而是确保单页应用的行为与用户习惯性的操作行为保存一致,更显著的是,当用户导航使用你的App时候,有以下几点问题:

  1. 在地址栏显示的URL总能够反映出视图展示的东西
  2. 能够成功地使用浏览器的返回和前进按钮
  3. 能够直接的使用相关联的URL导航到某个特别的视图
对于多页应用,这三点是不用去考虑的,也没有额外的你不得不去为多页应用考虑的。而对于单页应用,因为你不能导航到一个完成新的页面,你不得不真正的去处理这三个你的用户指出的问题。你需要确保导航进入你的App的URL是完全正确的。你需要确保浏览器的历史记录每个导航都是同步的来准许用户使用后退或前进按钮。如果用户标记(收藏)了某个特殊的视图或复制粘贴了一URL以便之后可以访问,你需要确保的是你的单页应用能够将用户引导正确的位置。

为了处理以上的问题,你需要有一门通常叫做路由的技术。

二、开始

二.一 构建App

create-react-app react_spa
cd react_spa
npm i react-router-dom --save

二、二 整理项目结构

将原来项目中publicsrc文件夹下的所有文件(夹)删除,然后在public文件夹下创建一个index.html将服务于我们App的开始入口。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport"
          content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <meta name="theme-color" content="#000000">
    <title>React Router Example</title>
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>

之后在src文件夹下创建一个index.js文件作为我们的入口文件,并添加下面内容:

import React from "react";
import ReactDOM from "react-dom";
import Main from "./Main";
 
ReactDOM.render(
  <Main/>, 
  document.getElementById("root")
);

接下来在src文件夹下面创建一个Main.js文件作为路由的配置文件:

import React, { Component } from "react";
 
class Main extends Component {
  render() {
    return (
        <div>
          <h1>Simple SPA</h1>
          <ul className="header">
            <li><a href="/">Home</a></li>
            <li><a href="/stuff">Stuff</a></li>
            <li><a href="/contact">Contact</a></li>
          </ul>
          <div className="content">
             
          </div>
        </div>
    );
  }
}
 
export default Main;

构建内容页面:

Home.js

import React, { Component } from "react";
 
class Home extends React.Component {
  render() {
    return (
      <div>
        <h2>HELLO</h2>
        <p>Cras facilisis urna ornare ex volutpat, et
        convallis erat elementum. Ut aliquam, ipsum vitae
        gravida suscipit, metus dui bibendum est, eget rhoncus nibh
        metus nec massa. Maecenas hendrerit laoreet augue
        nec molestie. Cum sociis natoque penatibus et magnis
        dis parturient montes, nascetur ridiculus mus.</p>
 
        <p>Duis a turpis sed lacus dapibus elementum sed eu lectus.</p>
      </div>
    );
  }
}
 
export default Home

Stuff.js

import React, { Component } from "react";
 
class Stuff extends React.Component {
  render() {
    return (
      <div>
        <h2>STUFF</h2>
        <p>Mauris sem velit, vehicula eget sodales vitae,
        rhoncus eget sapien:</p>
        <ol>
          <li>Nulla pulvinar diam</li>
          <li>Facilisis bibendum</li>
          <li>Vestibulum vulputate</li>
          <li>Eget erat</li>
          <li>Id porttitor</li>
        </ol>
      </div>
    );
  }
}
 
export default Stuff

Contact.js

import React, { Component } from "react";
 
class Contact extends React.Component {
  render() {
    return (
      <div>
        <h2>GOT QUESTIONS?</h2>
        <p>The easiest thing to do is post on
        our <a href="https://github.com/huangche007">GitHub</a>.
        </p>
      </div>
    );
  }
}
 
export default Contact

三、使用路由

Main组件中已经有了这个App的基本框架,也有了HomeStuffContact三个内容组件了,现在需要做的就是将它们连接起来构建成我们的App,这时候就需要使用到了React 路由了,回到Main.js,确保导入的语句如下:

import React, { Component } from "react";
import {
  Route,
  NavLink,
  HashRouter
} from "react-router-dom";
import Home from "./Home";
import Stuff from "./Stuff";
import Contact from "./Contact";

React路由工作方式被定义成一种称为路由区域,在路由区域里,有两件事可做:

  1. 导航的连接
  2. 将页面内容加载到容器里

导航的连接:在Main.js组件的render方法中增加<HashRouter>的代码:

class Main extends Component {
  render() {
    return (
     <HashRouter>
        <div>
          <h1>Simple SPA</h1>
          <ul className="header">
            <li><a href="/">Home</a></li>
            <li><a href="/stuff">Stuff</a></li>
            <li><a href="/contact">Contact</a></li>
          </ul>
          <div className="content">
             
          </div>
        </div>
      </HashRouter>
    );
  }
}

HashRouter组件为导航和由路由组成的浏览器历史记录操作提供了基础,接下来将定义导航的连接,使用特殊的NavLink组件替换掉a标签,并为这样的组件添加to属性.

class Main extends Component {
  render() {
    return (
      <HashRouter>
        <div>
          <h1>Simple SPA</h1>
          <ul className="header">
            <li><NavLink to="/">Home</NavLink></li>
            <li><NavLink to="/stuff">Stuff</NavLink></li>
            <li><NavLink to="/contact">Contact</NavLink></li>
          </ul>
          <div className="content">
             
          </div>
        </div>
      </HashRouter>
    );
  }
}

注意每个连接,将让路由得知导航到的URL,URL的值(通过to属性定义)作为一个标识来确保正确的内容得到加载,我们匹配内容的URL的方式在Route(路由)组件中得到使用

class Main extends Component {
  render() {
    return (
      <HashRouter>
        <div>
          <h1>Simple SPA</h1>
          <ul className="header">
            <li><NavLink to="/">Home</NavLink></li>
            <li><NavLink to="/stuff">Stuff</NavLink></li>
            <li><NavLink to="/contact">Contact</NavLink></li>
          </ul>
          <div className="content">
            <Route path="/" component={Home}/>
            <Route path="/stuff" component={Stuff}/>
            <Route path="/contact" component={Contact}/>
          </div>
        </div>
      </HashRouter>
    );
  }
}

正如你所见到的,Route组件包含一个path属性,指定的path值决定了路由何时被激活。当路由激活的时候,通过组件prop指定的组件将被渲染。比如:当点击Stuff链接的时候,path值为/stuff的路由变成了激活状态,也就意味着Stuff组件的内容得到了渲染。

接下来可以npm start 来看看效果了。

四、修复路由

当运行起来之后,会发现,无论我们切换的Stuff,Contact,Home组件的内容一直存在,这意味着无论导航到哪个路由,Home组件总是能够匹配上,这是不对的。可以在Home路由组件中,增加exact属性来解决。

<div className="content">
  <Route exact path="/" component={Home}/>
  <Route path="/stuff" component={Stuff}/>
  <Route path="/contact" component={Contact}/>
</div>

exact属性能够确保只有路由路径正确的匹配,那么这个路由组件才能将加载

五、效果图

图片描述

查看原文

zhouciming 赞了回答 · 2019-09-11

解决Vuejs和React如何选择?

React获取表单里面的值能让你把键盘砸了。 (搞不懂为什么到现在了都不优化这个功能)

关注 17 回答 15

zhouciming 收藏了文章 · 2019-08-15

vue+axios新手实践实现登陆

其实像这类的文章网上已经有很多很好的,写这篇文章,相当于是做个笔记,以防以后忘记
用到的:1、 vuex 2、axios 3、vue-route

登陆流程为:1、提交登陆表单,拿到后台返回的数据
2、将数据存入vuex

vuex配置

这里直接跳过安装之类的,百度一大堆,我直接上代码

// store index.js
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)
// 初始化时用sessionStore.getItem('token'),这样子刷新页面就无需重新登录
const state = {
  user: window.sessionStorage.getItem('user'),
  token: window.sessionStorage.getItem('token')
}
const mutations = {
  //将token保存到sessionStorage里,token表示登陆状态
  SET_TOKEN: (state, data) => {
    state.token = data
    window.sessionStorage.setItem('token', data) 
  },
  //获取用户名
  GET_USER: (state, data) => {
    // 把用户名存起来
    state.user = data
    window.sessionStorage.setItem('user', data)
  },
  //登出
  LOGOUT: (state) => {
    // 登出的时候要清除token
    state.token = null
    state.user = null
    window.sessionStorage.removeItem('token')
    window.sessionStorage.removeItem('user')
  }
}

const actions = {
}
export default new Vuex.Store({
  state,
  mutations,
  actions
})

1、我在这里是将登录状态token,和用户名user存在sessionStorage里,以便组件使用,如果token为true则表示用户已经登陆sessionStorage和token这两个东西很简单用法自行百度
2、不要忘了在main.js引入store,vue实例中也要加入store
main.js


import store from './store/index'

new Vue({
  el: '#app',
  router,
  store,
  components: { App },
  template: '<App/>'
})

vue-route配置

import Vue from 'vue'
import Router from 'vue-router'
import Login from '../components/Login'
import Activity from '../components/Activity'
import Index from '../components/Index'
import store from '../store/index'

Vue.use(Router)

const router = new Router({
  routes: [
    {
      path: '/',
      name: '/',
      component: Index
    },
    {
      path: '/login',
      name: 'login',
      component: Login
    },
    {
      path: '/activity',
      name: 'activity',
      component: Activity,
      meta: {
        requireAuth: true // 添加该字段,表示进入这个路由是需要登录的
      }
    }
  ]
})

// 注册全局钩子用来拦截导航
router.beforeEach((to, from, next) => {
  const token = store.state.token
  if (to.meta.requireAuth) { // 判断该路由是否需要登录权限
    if (token) { // 通过vuex state获取当前的token是否存在
      next()
    } else {
      console.log('该页面需要登陆')
      next({
        path: '/login'
        // query: {redirect: to.fullPath} // 将跳转的路由path作为参数,登录成功后跳转到该路由
      })
    }
  } else {
    next()
  }
})

export default router

这里我用到router.beforeEach来实现拦截登陆,
1、在需要验证的路由的meta里加入我们自己的requireAuth
2、router.beforeEach里通过requireAuth验证该组件是否需要登陆
3、验证token如果为flase就表示未登陆跳转到登录页

axios发送请求

submitLogin () {
  this.$refs.loginForm.validate(valid => {
    if (valid) {
      axios.post('/login', {
        user: this.loginForm.user,
        pass: this.loginForm.pass
      })
        .then((response) => {
          if (response.status === 200) {
            this.$store.commit('SET_TOKEN', response.data.token)
            this.$store.commit('GET_USER', response.data.user)
            this.$message({
              message: '登陆成功',
              type: 'success'
            })
            this.$router.push({name: 'activity'})
          }
        })
        .catch(function (error) {
          console.log(error)
        })
    } else {
      console.log('error submit!!')
      return false
    }
  })
},

后台我没写,是用mock.js拦截ajax请求
因为我用的是element-ui所以上面代码有一些直接无视,看核心的就行
1、在数据返回成功后用this.$store.commit来更新vuex里的数据
2、登陆成功后跳转this.$router.push()跳转页面,
这里注意,如果你在前面导航拦截的钩子用了query: {redirect: to.fullPath}的话,
这里就 用 this.$router.push(this.$route.query.redirect);这样页面就能跳到
你跳到登陆页面前要去的那个路由了

那个TOKEN我这里也没有使用,就是在页面请求的时候带上这个TOKEN,与后端核对。

查看原文

zhouciming 回答了问题 · 2019-08-09

解决thinkphp5中如何使用不同的.env文件

不同的环境,设置不同的配置参数文件,启动时,根据需要读取不同的配置文件

关注 4 回答 4

zhouciming 赞了回答 · 2019-08-09

解决thinkphp5中如何使用不同的.env文件

写个环境变量,代码里读取这个变量,并使用相应配置。

关注 4 回答 4

认证与成就

  • 获得 3 次点赞
  • 获得 3 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 3 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2015-01-30
个人主页被 130 人浏览