acfasj

acfasj 查看完整档案

填写现居城市  |  填写毕业院校  |  填写所在公司/组织 codepen.io/acfasj/ 编辑
编辑

又不是不能用

个人动态

acfasj 关注了用户 · 11月1日

csRyan @csryan

So you're passionate? How passionate? What actions does your passion lead you to do? If the heart doesn't find a perfect rhyme with the head, then your passion means nothing.

关注 68

acfasj 回答了问题 · 10月18日

ts引入axios后,axios方法使用不了

如下图所示, axios提供的.d.ts, 你import axios from 'axios'的时候, 匹配上的类型是AxiosStatic, 这个类型继承了AxiosInstance, 的缺是有get这个方法的. 然而你的报错提示的是 ‘typeof Axios’ 而不是 AxiosStatic, 是不是你自己覆盖了axios的类型定义
image.png

关注 2 回答 1

acfasj 回答了问题 · 10月18日

解决请问下怎么把带有active标签的项目显示到顶部的标题处

如果你说的是tab标签的title的话, 可以用 document.title 设置. 用选择器获取.active的内容将其赋值给document.title就行了

关注 3 回答 2

acfasj 回答了问题 · 8月28日

微信支付h5 chrome无法跳回 redirect_uri?

参考这个 https://developers.weixin.qq.com/community/develop/article/doc/000ae2118a8b38c3a80ae419251c13

跳转微信的链接的时候, 不要用 window.location.href, 使用window.location.replace就可以了, 具体为什么不知道

关注 8 回答 6

acfasj 赞了问题 · 3月1日

解决为什么在docker中服务要以前台方式启动?

docker中用pm2启动一个node服务。
如果按如下默认启动方式,从log中就会发现服务频繁的重新启动,提示端口被占用等异常信息

pm2 start index.js

但是如果按前台方式启动服务,就不会有问题

pm2 start index.js --no-daemon 

请问这是什么原因呢?

关注 5 回答 1

acfasj 收藏了问题 · 3月1日

为什么在docker中服务要以前台方式启动?

docker中用pm2启动一个node服务。
如果按如下默认启动方式,从log中就会发现服务频繁的重新启动,提示端口被占用等异常信息

pm2 start index.js

但是如果按前台方式启动服务,就不会有问题

pm2 start index.js --no-daemon 

请问这是什么原因呢?

acfasj 赞了回答 · 2月23日

现在优酷的网站支付宝支付,这样的文档在哪

不是当面付哦,你可以拿专业的二维码工具扫,是个(优酷自己的)h5链接,用微信和支付宝打开后会判断环境调用各自的sdk
你图中选的连续包月,应该对应的是支付宝的生活缴费产品
https://docs.open.alipay.com/...

关注 2 回答 2

acfasj 赞了回答 · 2月21日

解决sudo npm upgrade --global yarn不能更新yarn?

我也遇到了,卸了重新装
npm uninstall -g yarn && npm install -g yarn

关注 3 回答 2

acfasj 收藏了文章 · 2019-11-03

React Hooks 深入不浅出

这个标题可能不太好,但此文章确实不是一篇使用教程,而且也不会覆盖太多点,建议时间充裕的还是应该完整地看下 官网文档

React Hooks 对于部分人来说可能还是陌生的,但还是阻止不了它成为了当前 React 社区里「最」热门的一个词汇。

一开始了解到这个还是 Dan Abramov 在十月底的时候发了一个推,是一篇文章 Making Sense of React Hooks,建议没看过的先看下。看完第一感受就是:React 本就应该是这样的啊!

看完这篇文章,希望你可以从整体上对 Hooks 有个认识,并对其设计哲学有一些理解,希望看的过程不要急,跟着我的思路走。

如果你想自己跟着文章一起练手,需要把 reactreact-dom 更新到 16.7.0-alpha 及以上,如果配置了 ESLint,记得添加对应的 Plugin

插曲

长期以来很多人会把 Stateless ComponentFunctional Component 混为一谈,我会试着跟他们解释这不是一回事(不是一个维度),但当时 Functional Component 里确实无法使用 state,我无论怎么解释都会显得很无力。难道这冥冥之中都预示着会有类似 React Hooks 的东西出现?

React Hooks 的本质

稍微复杂点的项目肯定是充斥着大量的 React 生命周期函数(注意,即使你使用了状态管理库也避免不了这个),每个生命周期里几乎都承担着某个业务逻辑的一部分,或者说某个业务逻辑是分散在各个生命周期里的。

而 Hooks 的出现本质是把这种面向生命周期编程变成了面向业务逻辑编程,你不用再去关心本不该关心的生命周期。

一个 Hooks 演变

我们先假想一个常见的需求,一个 Modal 里需要展示一些信息,这些信息需要通过 API 获取且跟 Modal 强业务相关,要求我们:

  • 因为业务简单,没有引入额外状态管理库
  • 因为业务强相关,并不想把数据跟组件分开放
  • API 数据会随机变动,因此需要每次打开 Modal 才获取最新数据
  • 为了后期优化,不可以有额外的组件创建和销毁

我们可能的实现如下:

代码完整演示地址

class RandomUserModal extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      user: {},
      loading: false,
    };
    this.fetchData = this.fetchData.bind(this);
  }

  componentDidMount() {
    if (this.props.visible) {
      this.fetchData();
    }
  }

  componentDidUpdate(prevProps) {
    if (!prevProps.visible && this.props.visible) {
      this.fetchData();
    }
  }

  fetchData() {
    this.setState({ loading: true });
    fetch('https://randomuser.me/api/')
      .then(res => res.json())
      .then(json => this.setState({
        user: json.results[0],
        loading: false,
      }));
  }

  render() {
    const user = this.state.user;
    return (
      <ReactModal
        isOpen={this.props.visible}
      >
        <button onClick={this.props.handleCloseModal}>Close Modal</button>
        {this.state.loading ?
          <div>loading...</div>
          :
          <ul>
            <li>Name: {`${(user.name || {}).first} ${(user.name || {}).last}`}</li>
            <li>Gender: {user.gender}</li>
            <li>Phone: {user.phone}</li>
          </ul>
        }
      </ReactModal>
    )
  }
}

我们抽象了一个包含业务逻辑的 RandomUserModal,该 Modal 的展示与否由父组件控制,因此会传入参数 visiblehandleCloseModal(用于 Modal 关闭自己)。

为了实现在 Modal 打开的时候才进行数据获取,我们需要同时在 componentDidMountcomponentDidUpdate 两个生命周期里实现数据获取的逻辑,而且 constructor 里的一些初始化操作也少不了。

其实我们的要求很简单:在合适的时候通过 API 获取新的信息,这就是我们抽象出来的一个业务逻辑,为了这个业务逻辑能在 React 里正确工作,我们需要将其按照 React 组件生命周期进行拆解。这种拆解除了代码冗余,还很难复用

下面我们看看采用 Hooks 改造后会是什么样:

完整演示地址

function RandomUserModal(props) {
  const [user, setUser] = React.useState({});
  const [loading, setLoading] = React.useState(false);

  React.useEffect(() => {
    if (!props.visible) return;
    setLoading(true);
    fetch('https://randomuser.me/api/').then(res => res.json()).then(json => {
      setUser(json.results[0]);
      setLoading(false);
    });
  }, [props.visible]);
  
  return (
    // View 部分几乎与上面相同
  );
}

很明显地可以看到我们把 Class 形式变成了 Function 形式,使用了两个 State Hook 进行数据管理(类比 constructor),之前 cDMcDU 两个生命周期里干的事我们直接在一个 Effect Hook 里做了(如果有读取或修改 DOM 的需求可以看 这里)。做了这些,最大的优势是代码精简,业务逻辑变的紧凑,代码行数也从 50+ 行减少到 30+ 行。

Hooks 的强大之处还不仅仅是这个,最重要的是这些业务逻辑可以随意地的的抽离出去,跟普通的函数没什么区别(仅仅是看起来没区别),于是就变成了可以复用的自定义 Hook。具体可以看下面的进一步改造:

完整演示地址

// 自定义 Hook
function useFetchUser(visible) {
  const [user, setUser] = React.useState({});
  const [loading, setLoading] = React.useState(false);
  
  React.useEffect(() => {
    if (!visible) return;
    setLoading(true);
    fetch('https://randomuser.me/api/').then(res => res.json()).then(json => {
      setUser(json.results[0]);
      setLoading(false);
    });
  }, [visible]);
  return { user, loading };
}

function RandomUserModal(props) {
  const { user, loading } = useFetchUser(props.visible);
  
  return (
    // 与上面相同
  );
}

这里的 useFetchUser 为自定义 Hook,它的地位跟自带的 useState 等比也没什么区别,你可以在其它组件里使用,甚至在这个组件里使用两次,它们会天然地隔离开。

业务逻辑复用

这里说的业务逻辑复用主要是需要跨生命周期的业务逻辑。单单按照组件堆积的形式组织代码虽然也可以达到各种复用的目的,但是会导致组件非常复杂,数据流也会很乱。组件堆积适合 UI 布局,但是不适合逻辑组织。为了解决这些问题,在 React 发展过程中,产生了很多解决方案,我认知里常见的有以下几种:

Mixins

坏处远远大于带来的好处,因为现在已经不再支持,不多说,可以看看这篇文章:Mixins Considered Harmful

Class Inheritance

官方 很不推荐此做法,实际上我也没真的看到有人这么做。

High-Order Components (HOC)

React 高阶组件 在封装业务组件上简直是屡试不爽,它的实现是把自己作为一个函数,接受一个组件,再返回一个组件,这样它可以统一处理掉一些业务逻辑并达到复用目的。

比较常见的一个就是 react-redux 里的 connect 函数:

(图片来自 这里

但是它也被很多人吐槽嵌套问题:

(图片来自 这里

Render Props

Render Props 其实很常见,比如 React Context API

class App extends React.Component {
  render() {
    return (
      <ThemeProvider>
        <ThemeContext.Consumer>
          {val => <div>{val}</div>}
        </ThemeContext.Consumer>
      </ThemeProvider>
    )
  }
}

它的实现思路很简单,把原来该放「组件」的地方,换成了回调,这样当前组件里就可以拿到子组件的状态并使用。

但是,同样这会产生 Wrapper Hell 问题:

(图片来自 这里

Hooks

Hooks 本质上面说了,是把面向生命周期编程变成了面向业务逻辑编程,写法上带来的优化只是顺带的。

这里,做一个类比,await/async 本质是把 JS 里异步编程思维变成了同步思维,写法上表现出来的特点就是原来的 Callback Hell 被打平了。

总结对比:

  • await/async 把 Callback Hell 干掉了,异步编程思维变成了同步编程思维
  • Hooks 把 Wrapper Hell 干掉了,面向生命周期编程变成了面向业务逻辑编程

这里不得不客观地说,HOC 和 Render Props 还是有存在的必要,一方面是支持 React Class,另一方面,它们不光适用于纯逻辑封装,很多时候也适合逻辑 + 组件的封装场景,虽然此时使用 Hooks 也可以,但是会显得啰嗦点。另外,上面诟病的最大的问题 Wrapper Hell,我个人觉得使用 Fragment 也可以基本解决。

状态盒子

首先,React Hooks 的设计是反直觉的,为什么这样说呢?可以先试着问自己:为什么 Hooks 只能在其它 Hooks 的函数或者 React Function 组件里?

在我们的认知里,React 社区一直推崇函数式、纯函数等思想,引入 Hooks 概念后的 Functional Component 变的不再纯了,useXxx 与其说是一条执行语句,不如说是一个声明。声明这里放了一个「状态盒子」,盒子有输入和输出,剩下的内部实现就一无所知,重要的是,盒子是有记忆的,下次执行到此位置时,它有之前上下文信息。

类比「代码」和「程序」的区别,前者是死的,后者是活的。表达式 c = a + b 表示把 ab 累加后的值赋值给 c,但是如果写成 c := a + b 就表示 c 的值由 ab 相加得到。看起来表述差不多,但实际上,后者隐藏着一个时间的维度,它表示的是一种联系,而不单单是个运算。这在 RxJS 等库中被大量使用。

这种声明目前是通过很弱的 use 前缀标识的(但是设计上会简洁很多),为了不弄错每个盒子和状态的对应关系,书写的时候 Hooks 需要 use 开头且放在顶层作用域,即不可以包裹 if/switch/when/try 等。如果你按文章开头引入了那个 ESLint Plugin 就不用担心会弄错了。

总结

这篇文章可能并没有一个很条理的目录结构,大多是一些个人理解和相关思考。因此,这不能替代你去看真正的文档了解更多。如果你看完后还是觉得废话太多,不知所云,那我希望你至少可以在下面几点上跟作者达成共鸣:

  • Hooks 本质是把面向生命周期编程变成了面向业务逻辑编程
  • Hooks 使用上是一个逻辑状态盒子,输入输出表示的是一种联系;
  • Hooks 是 React 的未来,但还是无法完全替代原始的 Class。

参考

文章可随意转载,但请保留此 原文链接
非常欢迎有激情的你加入 ES2049 Studio,简历请发送至 caijun.hcj(at)alibaba-inc.com 。
查看原文

acfasj 赞了文章 · 2019-11-03

React Hooks 深入不浅出

这个标题可能不太好,但此文章确实不是一篇使用教程,而且也不会覆盖太多点,建议时间充裕的还是应该完整地看下 官网文档

React Hooks 对于部分人来说可能还是陌生的,但还是阻止不了它成为了当前 React 社区里「最」热门的一个词汇。

一开始了解到这个还是 Dan Abramov 在十月底的时候发了一个推,是一篇文章 Making Sense of React Hooks,建议没看过的先看下。看完第一感受就是:React 本就应该是这样的啊!

看完这篇文章,希望你可以从整体上对 Hooks 有个认识,并对其设计哲学有一些理解,希望看的过程不要急,跟着我的思路走。

如果你想自己跟着文章一起练手,需要把 reactreact-dom 更新到 16.7.0-alpha 及以上,如果配置了 ESLint,记得添加对应的 Plugin

插曲

长期以来很多人会把 Stateless ComponentFunctional Component 混为一谈,我会试着跟他们解释这不是一回事(不是一个维度),但当时 Functional Component 里确实无法使用 state,我无论怎么解释都会显得很无力。难道这冥冥之中都预示着会有类似 React Hooks 的东西出现?

React Hooks 的本质

稍微复杂点的项目肯定是充斥着大量的 React 生命周期函数(注意,即使你使用了状态管理库也避免不了这个),每个生命周期里几乎都承担着某个业务逻辑的一部分,或者说某个业务逻辑是分散在各个生命周期里的。

而 Hooks 的出现本质是把这种面向生命周期编程变成了面向业务逻辑编程,你不用再去关心本不该关心的生命周期。

一个 Hooks 演变

我们先假想一个常见的需求,一个 Modal 里需要展示一些信息,这些信息需要通过 API 获取且跟 Modal 强业务相关,要求我们:

  • 因为业务简单,没有引入额外状态管理库
  • 因为业务强相关,并不想把数据跟组件分开放
  • API 数据会随机变动,因此需要每次打开 Modal 才获取最新数据
  • 为了后期优化,不可以有额外的组件创建和销毁

我们可能的实现如下:

代码完整演示地址

class RandomUserModal extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      user: {},
      loading: false,
    };
    this.fetchData = this.fetchData.bind(this);
  }

  componentDidMount() {
    if (this.props.visible) {
      this.fetchData();
    }
  }

  componentDidUpdate(prevProps) {
    if (!prevProps.visible && this.props.visible) {
      this.fetchData();
    }
  }

  fetchData() {
    this.setState({ loading: true });
    fetch('https://randomuser.me/api/')
      .then(res => res.json())
      .then(json => this.setState({
        user: json.results[0],
        loading: false,
      }));
  }

  render() {
    const user = this.state.user;
    return (
      <ReactModal
        isOpen={this.props.visible}
      >
        <button onClick={this.props.handleCloseModal}>Close Modal</button>
        {this.state.loading ?
          <div>loading...</div>
          :
          <ul>
            <li>Name: {`${(user.name || {}).first} ${(user.name || {}).last}`}</li>
            <li>Gender: {user.gender}</li>
            <li>Phone: {user.phone}</li>
          </ul>
        }
      </ReactModal>
    )
  }
}

我们抽象了一个包含业务逻辑的 RandomUserModal,该 Modal 的展示与否由父组件控制,因此会传入参数 visiblehandleCloseModal(用于 Modal 关闭自己)。

为了实现在 Modal 打开的时候才进行数据获取,我们需要同时在 componentDidMountcomponentDidUpdate 两个生命周期里实现数据获取的逻辑,而且 constructor 里的一些初始化操作也少不了。

其实我们的要求很简单:在合适的时候通过 API 获取新的信息,这就是我们抽象出来的一个业务逻辑,为了这个业务逻辑能在 React 里正确工作,我们需要将其按照 React 组件生命周期进行拆解。这种拆解除了代码冗余,还很难复用

下面我们看看采用 Hooks 改造后会是什么样:

完整演示地址

function RandomUserModal(props) {
  const [user, setUser] = React.useState({});
  const [loading, setLoading] = React.useState(false);

  React.useEffect(() => {
    if (!props.visible) return;
    setLoading(true);
    fetch('https://randomuser.me/api/').then(res => res.json()).then(json => {
      setUser(json.results[0]);
      setLoading(false);
    });
  }, [props.visible]);
  
  return (
    // View 部分几乎与上面相同
  );
}

很明显地可以看到我们把 Class 形式变成了 Function 形式,使用了两个 State Hook 进行数据管理(类比 constructor),之前 cDMcDU 两个生命周期里干的事我们直接在一个 Effect Hook 里做了(如果有读取或修改 DOM 的需求可以看 这里)。做了这些,最大的优势是代码精简,业务逻辑变的紧凑,代码行数也从 50+ 行减少到 30+ 行。

Hooks 的强大之处还不仅仅是这个,最重要的是这些业务逻辑可以随意地的的抽离出去,跟普通的函数没什么区别(仅仅是看起来没区别),于是就变成了可以复用的自定义 Hook。具体可以看下面的进一步改造:

完整演示地址

// 自定义 Hook
function useFetchUser(visible) {
  const [user, setUser] = React.useState({});
  const [loading, setLoading] = React.useState(false);
  
  React.useEffect(() => {
    if (!visible) return;
    setLoading(true);
    fetch('https://randomuser.me/api/').then(res => res.json()).then(json => {
      setUser(json.results[0]);
      setLoading(false);
    });
  }, [visible]);
  return { user, loading };
}

function RandomUserModal(props) {
  const { user, loading } = useFetchUser(props.visible);
  
  return (
    // 与上面相同
  );
}

这里的 useFetchUser 为自定义 Hook,它的地位跟自带的 useState 等比也没什么区别,你可以在其它组件里使用,甚至在这个组件里使用两次,它们会天然地隔离开。

业务逻辑复用

这里说的业务逻辑复用主要是需要跨生命周期的业务逻辑。单单按照组件堆积的形式组织代码虽然也可以达到各种复用的目的,但是会导致组件非常复杂,数据流也会很乱。组件堆积适合 UI 布局,但是不适合逻辑组织。为了解决这些问题,在 React 发展过程中,产生了很多解决方案,我认知里常见的有以下几种:

Mixins

坏处远远大于带来的好处,因为现在已经不再支持,不多说,可以看看这篇文章:Mixins Considered Harmful

Class Inheritance

官方 很不推荐此做法,实际上我也没真的看到有人这么做。

High-Order Components (HOC)

React 高阶组件 在封装业务组件上简直是屡试不爽,它的实现是把自己作为一个函数,接受一个组件,再返回一个组件,这样它可以统一处理掉一些业务逻辑并达到复用目的。

比较常见的一个就是 react-redux 里的 connect 函数:

(图片来自 这里

但是它也被很多人吐槽嵌套问题:

(图片来自 这里

Render Props

Render Props 其实很常见,比如 React Context API

class App extends React.Component {
  render() {
    return (
      <ThemeProvider>
        <ThemeContext.Consumer>
          {val => <div>{val}</div>}
        </ThemeContext.Consumer>
      </ThemeProvider>
    )
  }
}

它的实现思路很简单,把原来该放「组件」的地方,换成了回调,这样当前组件里就可以拿到子组件的状态并使用。

但是,同样这会产生 Wrapper Hell 问题:

(图片来自 这里

Hooks

Hooks 本质上面说了,是把面向生命周期编程变成了面向业务逻辑编程,写法上带来的优化只是顺带的。

这里,做一个类比,await/async 本质是把 JS 里异步编程思维变成了同步思维,写法上表现出来的特点就是原来的 Callback Hell 被打平了。

总结对比:

  • await/async 把 Callback Hell 干掉了,异步编程思维变成了同步编程思维
  • Hooks 把 Wrapper Hell 干掉了,面向生命周期编程变成了面向业务逻辑编程

这里不得不客观地说,HOC 和 Render Props 还是有存在的必要,一方面是支持 React Class,另一方面,它们不光适用于纯逻辑封装,很多时候也适合逻辑 + 组件的封装场景,虽然此时使用 Hooks 也可以,但是会显得啰嗦点。另外,上面诟病的最大的问题 Wrapper Hell,我个人觉得使用 Fragment 也可以基本解决。

状态盒子

首先,React Hooks 的设计是反直觉的,为什么这样说呢?可以先试着问自己:为什么 Hooks 只能在其它 Hooks 的函数或者 React Function 组件里?

在我们的认知里,React 社区一直推崇函数式、纯函数等思想,引入 Hooks 概念后的 Functional Component 变的不再纯了,useXxx 与其说是一条执行语句,不如说是一个声明。声明这里放了一个「状态盒子」,盒子有输入和输出,剩下的内部实现就一无所知,重要的是,盒子是有记忆的,下次执行到此位置时,它有之前上下文信息。

类比「代码」和「程序」的区别,前者是死的,后者是活的。表达式 c = a + b 表示把 ab 累加后的值赋值给 c,但是如果写成 c := a + b 就表示 c 的值由 ab 相加得到。看起来表述差不多,但实际上,后者隐藏着一个时间的维度,它表示的是一种联系,而不单单是个运算。这在 RxJS 等库中被大量使用。

这种声明目前是通过很弱的 use 前缀标识的(但是设计上会简洁很多),为了不弄错每个盒子和状态的对应关系,书写的时候 Hooks 需要 use 开头且放在顶层作用域,即不可以包裹 if/switch/when/try 等。如果你按文章开头引入了那个 ESLint Plugin 就不用担心会弄错了。

总结

这篇文章可能并没有一个很条理的目录结构,大多是一些个人理解和相关思考。因此,这不能替代你去看真正的文档了解更多。如果你看完后还是觉得废话太多,不知所云,那我希望你至少可以在下面几点上跟作者达成共鸣:

  • Hooks 本质是把面向生命周期编程变成了面向业务逻辑编程
  • Hooks 使用上是一个逻辑状态盒子,输入输出表示的是一种联系;
  • Hooks 是 React 的未来,但还是无法完全替代原始的 Class。

参考

文章可随意转载,但请保留此 原文链接
非常欢迎有激情的你加入 ES2049 Studio,简历请发送至 caijun.hcj(at)alibaba-inc.com 。
查看原文

赞 71 收藏 45 评论 2

acfasj 赞了回答 · 2019-09-08

解决cms后台发布文章时的‘预览’功能实现思路是什么?

一些富文本编辑器提供了预览功能,但事实上这个预览与前台效果存在极大的差异,同时一些开源产品提供了更好更真实的实现,如wordpress, durpal等。
富文本编辑器的预览偏差很大的原因就是css设置导致,同时体会这些更好更真实的开源产品可以发现一个共性,就是草稿功能,因此或者我们能推理出这样一个思路。

文章在预览之前需要进行保存,使其成为一个“持久化”的数据,而后在特定的容器内(如iframe)加载前端的css,并再此基础上显示文章的内容,从而看到该文章在前端显示时的最终效果。

加油,虽然说起来很简单,但是实际做起来不容易,太多的细节需要处理了。

关注 2 回答 1

acfasj 赞了文章 · 2019-08-29

拿Proxy可以做哪些有意思的事儿

Proxy是什么

首先,我们要清楚,Proxy是什么意思,这个单词翻译过来,就是 代理
可以理解为,有一个很火的明星,开通了一个微博账号,这个账号非常活跃,回复粉丝、到处点赞之类的,但可能并不是真的由本人在维护的。
而是在背后有一个其他人 or 团队来运营,我们就可以称他们为代理人,因为他们发表的微博就代表了明星本人的意思。
P.S. 强行举例子,因为本人不追星,只是猜测可能会有这样的运营团队

这个代入到JavaScript当中来,就可以理解为对对象或者函数的代理操作。

JavaScript中的Proxy

Proxy是ES6中提供的新的API,可以用来定义对象各种基本操作的自定义行为
(在文档中被称为traps,我觉得可以理解为一个针对对象各种行为的钩子)
拿它可以做很多有意思的事情,在我们需要对一些对象的行为进行控制时将变得非常有效。

Proxy的语法

创建一个Proxy的实例需要传入两个参数

  1. target 要被代理的对象,可以是一个object或者function
  2. handlers对该代理对象的各种操作行为处理
let target = {}
let handlers = {} // do nothing
let proxy = new Proxy(target, handlers)

proxy.a = 123

console.log(target.a) // 123

在第二个参数为空对象的情况下,基本可以理解为是对第一个参数做的一次浅拷贝
(Proxy必须是浅拷贝,如果是深拷贝则会失去了代理的意义)

Traps(各种行为的代理)

就像上边的示例代码一样,如果没有定义对应的trap,则不会起任何作用,相当于直接操作了target
当我们写了某个trap以后,在做对应的动作时,就会触发我们的回调函数,由我们来控制被代理对象的行为。

最常用的两个trap应该就是getset了。
早年JavaScript有着在定义对象时针对某个属性进行设置gettersetter

let obj = {
  _age: 18,
  get age ()  {
    return `I'm ${this._age} years old`
  },
  set age (val) {
    this._age = Number(val)
  }
}

console.log(obj.age) // I'm 18 years old
obj.age = 19
console.log(obj.age) // I'm 19 years old

就像这段代码描述的一样,我们设置了一个属性_age,然后又设置了一个get ageset age
然后我们可以直接调用obj.age来获取一个返回值,也可以对其进行赋值。
这么做有几个缺点:

  1. 针对每一个要代理的属性都要编写对应的gettersetter
  2. 必须还要存在一个存储真实值的key(如果我们直接在getter里边调用this.age则会出现堆栈溢出的情况,因为无论何时调用this.age进行取值都会触发getter

Proxy很好的解决了这两个问题:

let target = { age: 18, name: 'Niko Bellic' }
let handlers = {
  get (target, property) {
    return `${property}: ${target[property]}`
  },
  set (target, property, value) {
    target[property] = value
  }
}
let proxy = new Proxy(target, handlers)

proxy.age = 19
console.log(target.age, proxy.age)   // 19,          age : 19
console.log(target.name, proxy.name) // Niko Bellic, name: Niko Bellic

我们通过创建getset两个trap来统一管理所有的操作,可以看到,在修改proxy的同时,target的内容也被修改,而且我们对proxy的行为进行了一些特殊的处理。
而且我们无需额外的用一个key来存储真实的值,因为我们在trap内部操作的是target对象,而不是proxy对象。

拿Proxy来做些什么

因为在使用了Proxy后,对象的行为基本上都是可控的,所以我们能拿来做一些之前实现起来比较复杂的事情。
在下边列出了几个简单的适用场景。

解决对象属性为undefined的问题

在一些层级比较深的对象属性获取中,如何处理undefined一直是一个痛苦的过程,如果我们用Proxy可以很好的兼容这种情况。

(() => {
  let target = {}
  let handlers = {
    get: (target, property) => {
      target[property] = (property in target) ? target[property] : {}
      if (typeof target[property] === 'object') {
        return new Proxy(target[property], handlers)
      }
      return target[property]
    }
  }
  let proxy = new Proxy(target, handlers)
  console.log('z' in proxy.x.y) // false (其实这一步已经针对`target`创建了一个x.y的属性)
  proxy.x.y.z = 'hello'
  console.log('z' in proxy.x.y) // true
  console.log(target.x.y.z)     // hello
})()

我们代理了get,并在里边进行逻辑处理,如果我们要进行get的值来自一个不存在的key,则我们会在target中创建对应个这个key,然后返回一个针对这个key的代理对象。
这样就能够保证我们的取值操作一定不会抛出can not get xxx from undefined
但是这会有一个小缺点,就是如果你确实要判断这个key是否存在只能够通过in操作符来判断,而不能够直接通过get来判断。

普通函数与构造函数的兼容处理

如果我们提供了一个Class对象给其他人,或者说一个ES5版本的构造函数。
如果没有使用new关键字来调用的话,Class对象会直接抛出异常,而ES5中的构造函数this指向则会变为调用函数时的作用域。
我们可以使用apply这个trap来兼容这种情况:

class Test {
  constructor (a, b) {
    console.log('constructor', a, b)
  }
}

// Test(1, 2) // throw an error
let proxyClass = new Proxy(Test, {
  apply (target, thisArg, argumentsList) {
    // 如果想要禁止使用非new的方式来调用函数,直接抛出异常即可
    // throw new Error(`Function ${target.name} cannot be invoked without 'new'`)
    return new (target.bind(thisArg, ...argumentsList))()
  }
})

proxyClass(1, 2) // constructor 1 2

我们使用了apply来代理一些行为,在函数调用时会被触发,因为我们明确的知道,代理的是一个Class或构造函数,所以我们直接在apply中使用new关键字来调用被代理的函数。

以及如果我们想要对函数进行限制,禁止使用new关键字来调用,可以用另一个trap:construct

function add (a, b) {
  return a + b
}

let proxy = new Proxy(add, {
  construct (target, argumentsList, newTarget) {
    throw new Error(`Function ${target.name} cannot be invoked with 'new'`)
  }
})

proxy(1, 2)     // 3
new proxy(1, 2) // throw an error

用Proxy来包装fetch

在前端发送请求,我们现在经常用到的应该就是fetch了,一个原生提供的API。
我们可以用Proxy来包装它,使其变得更易用。

let handlers = {
  get (target, property) {
    if (!target.init) {
      // 初始化对象
      ['GET', 'POST'].forEach(method => {
        target[method] = (url, params = {}) => {
          return fetch(url, {
            headers: {
              'content-type': 'application/json'
            },
            mode: 'cors',
            credentials: 'same-origin',
            method,
            ...params
          }).then(response => response.json())
        }
      })
    }

    return target[property]
  }
}
let API = new Proxy({}, handlers)

await API.GET('XXX')
await API.POST('XXX', {
  body: JSON.stringify({name: 1})
})

GETPOST进行了一层封装,可以直接通过.GET这种方式来调用,并设置一些通用的参数。

实现一个简易的断言工具

写过测试的各位童鞋,应该都会知道断言这个东西
console.assert就是一个断言工具,接受两个参数,如果第一个为false,则会将第二个参数作为Error message抛出。
我们可以使用Proxy来做一个直接赋值就能实现断言的工具。

let assert = new Proxy({}, {
  set (target, message, value) {
    if (!value) console.error(message)
  }
})

assert['Isn\'t true'] = false      // Error: Isn't true
assert['Less than 18'] = 18 >= 19  // Error: Less than 18

统计函数调用次数

在做服务端时,我们可以用Proxy代理一些函数,来统计一段时间内调用的次数。
在后期做性能分析时可能会能够用上:

function orginFunction () {}
let proxyFunction = new Proxy(orginFunction, {
  apply (target, thisArg. argumentsList) {
    log(XXX)

    return target.apply(thisArg, argumentsList)
  }
})

全部的traps

这里列出了handlers所有可以定义的行为 (traps)

具体的可以查看MDN-Proxy
里边同样有一些例子
trapsdescription
get获取某个key
set设置某个key
has使用in操作符判断某个key是否存在
apply函数调用,仅在代理对象为function时有效
ownKeys获取目标对象所有的key
construct函数通过实例化调用,仅在代理对象为function时有效
isExtensible判断对象是否可扩展,Object.isExtensible的代理
deleteProperty删除一个property
defineProperty定义一个新的property
getPrototypeOf获取原型对象
setPrototypeOf设置原型对象
preventExtensions设置对象为不可扩展
getOwnPropertyDescriptor获取一个自有属性 (不会去原型链查找) 的属性描述

参考资料

  1. Magic Methods in JavaScript? Meet Proxy!
  2. How to use JavaScript Proxies for Fun and Profit
  3. MDN-Proxy
查看原文

赞 77 收藏 95 评论 2

acfasj 赞了问题 · 2019-07-15

react 的弹出层你们一般怎么写

最近写了个弹出层的表单
发现写的不是很好
你们一般怎么写 最好有demo

关注 5 回答 3

acfasj 收藏了问题 · 2019-07-15

react 的弹出层你们一般怎么写

最近写了个弹出层的表单
发现写的不是很好
你们一般怎么写 最好有demo

acfasj 收藏了问题 · 2019-07-15

css样式 body的font-size 为什么用62.5%而不是10px;

浏览器的默认高度?
一般为16px.

为什么用62.5%作为body的默认样式?
16px62.5%=10px.*

那么为什么一般多是

.body{font-size:62.5%;}

而不是

.body{font-size:10px;}

acfasj 赞了问题 · 2019-07-15

解决css样式 body的font-size 为什么用62.5%而不是10px;

浏览器的默认高度?
一般为16px.

为什么用62.5%作为body的默认样式?
16px62.5%=10px.*

那么为什么一般多是

.body{font-size:62.5%;}

而不是

.body{font-size:10px;}

关注 12 回答 4

acfasj 收藏了文章 · 2019-06-05

React 实现 Table 的思考

Table 是最常用展示数据的方式之一,可是一个产品中往往很多非常类似的 Table,但是我们碰到的情况往往是 Table A 要排序,Table B 不需要排序,等等这种看起来非常类似,但是又不完全相同的表格。这种情况下,到底要不要抽取一个公共的 Table 组件呢?对于这个问题,我们团队也纠结了很久,先后开发了多个版本的 Table 组件,在最近的一个项目中,产出了第三版 Table 组件,能够较好的解决灵活性和公共逻辑抽取的问题。本文将会详细的讲述这种 Table 组件解决方案产出的过程和一些思考。

Table 的常见实现

首先我们看到的是不使用任何组件实现一个业务表格的代码:

import React, { Component } from 'react';

const columnOpts = [
  { key: 'a', name: 'col-a' },
  { key: 'b', name: 'col-b' },
];

function SomeTable(props) {
  const { data } = props;

  return (
    <div className="some-table">
      <ul className="table-header">
        {
          columnOpts.map((opt, colIndex) => (
            <li key={`col-${colIndex}`}>{opt.name}</li>
          ))
        }
      </ul>
      <ul className="table-body">
        {
          data.map((entry, rowIndex) => (
            <li key={`row-${rowIndex}`}>
              {
                columnOpts.map((opt, colIndex) => (
                  <span key={`col-${colIndex}`}>{entry[opt.key]}</span>
                ))
              }
            </li>
          ))
        }
      </ul>
    </div>
  );
}

这种实现方法带来的问题是:

  • 每次写表格需要写很多布局类的样式

  • 重复代码很多,而且项目成员之间很难达到统一,A 可能喜欢用表格来布局,B 可能喜欢用 ul 来布局

  • 相似但是不完全相同的表格很难复用

抽象过程

组件是对数据和方法的一种封装,在封装之前,我们总结了一下表格型的展示的特点:

  • 输入数据源较统一,一般为对象数组

  • thead 中的单元格大部分只是展示一些名称,也有一些个性化的内容,如带有排序 icon 的单元格

  • tbody 中的部分单元格只是简单的读取一些值,很多单元格的都有自己的逻辑,但是在一个产品中通常很多类似的单元格

  • 列是有顺序的,更适合以列为单位来添加布局样式

基于以上特点,我们希望 Table 组件能够满足以下条件:

  • 接收一个 对象数组所有列的配置 为参数,自动创建基础的表格内容

  • thead 和 tbody 中的单元格都能够定制化,以满足不同的需求

至此,我们首先想到 Table 组件应该长成这样的:

const columnOpts =  [
  { key: 'a', name: 'col-a', onRenderTd: () => {} },
  { key: 'b', name: 'col-b', onRenderTh: () => {}, onRenderTd: () => {} },
];

<Table data={data} columnOpts={columnOpts} />

其中 onRenderTdonRenderTh 分别是渲染 td 和 th 时的回调函数。

到这里我们发现对于稍微复杂一点的 table,columnOpts 将会是一个非常大的配置数组,我们有没有办法不使用数组来维护这些配置呢?这里我们想到的一个办法是创建一个 Column 的组件,让大家可以这么来写这个 table:

<Table data={data}>
  <Column dataKey="a" name="col-a" td={onRenderTd} />
  <Column dataKey="b" name="col-b" td={onRenderTd} th={onRenderTh} />
</Table>

这样大家就可以像写HTML一样把一个简单的表格给搭建出来了。

优化

有了 Table 的雏形,再联系下写表格的常见需求,我们给 Column 添加了 widthalign 属性。加这两个属性的原因很容易想到,因为我们在写表格相关业务时,样式里面写的最多的就是单元格的宽度和对齐方式。我们来看一下 Column 的实现:

import React, { PropTypes, Component } from 'react';

const propTypes = {
  name: PropTypes.string,
  dataKey: PropTypes.string.isRequired,
  align: PropTypes.oneOf(['left', 'center', 'right']),
  width: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
  th: PropTypes.oneOfType([PropTypes.element, PropTypes.func]),
  td: PropTypes.oneOfType([
    PropTypes.element, PropTypes.func, PropTypes.oneOf([
      'int', 'float', 'percent', 'changeRate'
    ])
  ]),
};

const defaultProps = {
  align: 'left',
};

function Column() {
  return null;
}

Column.propTypes = propTypes;
Column.defaultProps = defaultProps;

export default Column;

代码中可以发现 th 可以接收两种格式,一种是 function,一种是 ReactElement。这里提供 ReactElement 类型的 th 主要让大家能够设置一些额外的 props,后面我们会给出一个例子。

td 的类型就更复杂了,不仅能够接收 functionReactElement 这两种类型,还有 int, float, percent, changeRate 这三种类型是最常用的数据类型,这样方便我们可以在 Table 里面根据类型对数据做格式化,省去了项目成员中很多重复的代码。

下面我们看一下 Table 的实现:

const getDisplayName = (el) => {
  return el && el.type && (el.type.displayName || el.type.name);
};

const renderChangeRate = (changeRate) => { ... };

const renderThs = (columns) => {
  return columns.map((col, index) => {
    const { name, dataKey, th } = col.props;
    const props = { name, dataKey, colIndex: index };
    let content;
    let className;

    if (React.isValidElement(th)) {
      content = React.cloneElement(th, props);
      className = getDisplayName(th);
    } else if (_.isFunction(th)) {
      content = th(props);
    } else {
      content = name || '';
    }

    return (
      <th
        key={`th-${index}`}
        style={getStyle(col.props)}
        className={`table-th col-${index} col-${dataKey} ${className || ''}`}
      >
        {content}
      </th>
    );
  });
};

const renderTds = (data, entry, columns, rowIndex) => {
  return columns.map((col, index) => {
    const { dataKey, td } = col.props;
    const value = getValueOfTd(entry, dataKey);
    const props = { data, rowData: entry, tdValue: value, dataKey, rowIndex, colIndex: index };

    let content;
    let className;
    if (React.isValidElement(td)) {
      content = React.cloneElement(td, props);
      className = getDisplayName(td);
    } else if (td === 'changeRate') {
      content = renderChangeRate(value || '');
    } else if (_.isFunction(td)) {
      content = td(props);
    } else {
      content = formatIndex(parseValueOfTd(value), dataKey, td);
    }

    return (
      <td
        key={`td-${index}`}
        style={getStyle(col.props)}
        className={`table-td col-${index} col-${dataKey} ${className || ''}`}
      >
        {content}
      </td>
    );
  });
};

const renderRows = (data, columns) => {
  if (!data || !data.length) {return null;}

  return data.map((entry, index) => {
    return (
      <tr className="table-tbody-tr" key={`tr-${index}`}>
        {renderTds(data, entry, columns, index)}
      </tr>
    );
  });
};

function Table(props) {
  const { children, data, className } = props;
  const columns = findChildrenByType(children, Column);

  return (
    <div className={`table-container ${className || ''}`}>
      <table className="base-table">
        {hasNames(columns) && (
          <thead>
            <tr className="table-thead-tr">
              {renderThs(columns)}
            </tr>
          </thead>
        )}
        <tbody>{renderRows(data, columns)}</tbody>
      </table>
    </div>
  );
}

代码说明了一切,就不再详细说了。当然,在业务组件里,还可以加上公共的错误处理逻辑。

单元格示例

前面提到我们的 tdth 还可以接收 ReactElement 格式的 props,大家可能还有会有点疑惑,下面我们看一个 SortableTh 的例子:

class SortableTh extends Component {
 static displayName = 'SortableTh';

 static propTypes = {
    ...,
    initialOrder: PropTypes.oneOf(['asc', 'desc']),
    order: PropTypes.oneOf(['asc', 'desc', 'none']).isRequired,
    onChange: PropTypes.func.isRequired,
 };

 static defaultProps = {
   order: 'none',
   initialOrder: 'desc',
 };

 onClick = () => {
   const { onChange, initialOrder, order, dataKey } = this.props;

   if (dataKey) {
     let nextOrder = 'none';

     if (order === 'none') {
       nextOrder = initialOrder;
     } else if (order === 'desc') {
       nextOrder = 'asc';
     } else if (order === 'asc') {
       nextOrder = 'desc';
     }

     onChange({ orderBy: dataKey, order: nextOrder });
   }
 };

 render() {
   const { name, order, hasRate, rateType } = this.props;

   return (
     <div className="sortable-th" onClick={this.onClick}>
       <span>{name}</span>
       <SortIcon order={order} />
     </div>
   );
 }
}

通过这个例子可以看到,thtd 接收 ReactElement 类型的 props 能够让外部很好的控制单元格的内容,每个单元格不只是接收 data 数据的封闭单元。

总结

总结一些自己的感想:

  • 前端工程师也需要往前走一步,了解用户习惯。在写这个组件之前,我一直是用 ul 来写表格的,用 ul 写的表格调整样式比较便利,后来发现用户很多时候喜欢把整个表格里面的内容 copy 下来用于存档。然而,ul 写的表格 copy 后粘贴在 excel 中,整行的内容都在一个单元格里面,用 table 写的表格则能够几乎保持原本的格式,所以我们这次用了原生的 table 来写表格。

  • 业务代码中组件抽取的粒度一直是一个比较纠结的问题。粒度太粗,项目成员之间需要写很多重复的代码。粒度太细,后续可扩展性又很低,所以只能是大家根据业务特点来评估了。像 Table 这样的组件非常通用,而且后续肯定有新的类型冒出来,所以粒度不宜太细。当然,我们这样写 Table 组件后,大家可以抽取常用的一些 XXXThXXXTd

最终,我把这次 Table 组件的经验抽离出来,开源到 https://github.com/recharts/react-smart-table,希望开发者们可以参考。

查看原文

acfasj 赞了文章 · 2019-05-29

React 中同构(SSR)原理脉络梳理

react-server-side-rendering.jpg

随着越来越多新型前端框架的推出,SSR 这个概念在前端开发领域的流行度越来越高,也有越来越多的项目采用这种技术方案进行了实现。SSR 产生的背景是什么?适用的场景是什么?实现的原理又是什么?希望大家在这篇文章中能够找到你想要的答案。

说到 SSR,很多人的第一反应是“服务器端渲染”,但我更倾向于称之为“同构”,所以首先我们来对“客户端渲染”,“服务器端渲染”,“同构”这三个概念简单的做一个分析:

客户端渲染:客户端渲染,页面初始加载的 HTML 页面中无网页展示内容,需要加载执行JavaScript 文件中的 React 代码,通过 JavaScript 渲染生成页面,同时,JavaScript 代码会完成页面交互事件的绑定,详细流程可参考下图(图片取材自 fullstackacademy.com):

client-side-rendering.jpg

服务器端渲染:用户请求服务器,服务器上直接生成 HTML 内容并返回给浏览器。服务器端渲染来,页面的内容是由 Server 端生成的。一般来说,服务器端渲染的页面交互能力有限,如果要实现复杂交互,还是要通过引入 JavaScript 文件来辅助实现。服务器端渲染这个概念,适用于任何后端语言。

server-side-rendering.jpg

同构:同构这个概念存在于 Vue,React 这些新型的前端框架中,同构实际上是客户端渲染和服务器端渲染的一个整合。我们把页面的展示内容和交互写在一起,让代码执行两次。在服务器端执行一次,用于实现服务器端渲染,在客户端再执行一次,用于接管页面交互,详细流程可参考下图(图片取材自 fullstackacademy.com):

ssr.jpg

一般情况下,当我们使用 React 编写代码时,页面都是由客户端执行 JavaScript 逻辑动态挂 DOM 生成的,也就是说这种普通的单页面应用实际上采用的是客户端渲染模式。在大多数情况下,客户端渲染完全能够满足我们的业务需求,那为什么我们还需要 SSR 这种同构技术呢?

使用 SSR 技术的主要因素:

  1. CSR 项目的 TTFP(Time To First Page)时间比较长,参考之前的图例,在 CSR 的页面渲染流程中,首先要加载 HTML 文件,之后要下载页面所需的 JavaScript 文件,然后 JavaScript 文件渲染生成页面。在这个渲染过程中至少涉及到两个 HTTP 请求周期,所以会有一定的耗时,这也是为什么大家在低网速下访问普通的 React 或者 Vue 应用时,初始页面会有出现白屏的原因。
  2. CSR 项目的 SEO 能力极弱,在搜索引擎中基本上不可能有好的排名。因为目前大多数搜索引擎主要识别的内容还是 HTML,对 JavaScript 文件内容的识别都还比较弱。如果一个项目的流量入口来自于搜索引擎,这个时候你使用 CSR 进行开发,就非常不合适了。

SSR 的产生,主要就是为了解决上面所说的两个问题。在 React 中使用 SSR 技术,我们让 React 代码在服务器端先执行一次,使得用户下载的 HTML 已经包含了所有的页面展示内容,这样,页面展示的过程只需要经历一个 HTTP 请求周期,TTFP 时间得到一倍以上的缩减。

同时,由于 HTML 中已经包含了网页的所有内容,所以网页的 SEO 效果也会变的非常好。之后,我们让 React 代码在客户端再次执行,为 HTML 网页中的内容添加数据及事件的绑定,页面就具备了 React 的各种交互能力。

但是,SSR 这种理念的实现,并非易事。我们来看一下在 React 中实现 SSR 技术的架构图:

ssr-framework.jpg

使用 SSR 这种技术,将使原本简单的 React 项目变得非常复杂,项目的可维护性会降低,代码问题的追溯也会变得困难。

所以,使用 SSR 在解决问题的同时,也会带来非常多的副作用,有的时候,这些副作用的伤害比起 SSR 技术带来的优势要大的多。从个人经验上来说,我一般建议大家,除非你的项目特别依赖搜索引擎流量,或者对首屏时间有特殊的要求,否则不建议使用 SSR。

好,如果你确实遇到了 React 项目中要使用 SSR 的场景并决定使用 SSR,那么接下来我们就结合上面这张 SSR 架构图,开启 SSR 技术点的难点剖析。

在开始之前,我们先来分析下虚拟 DOM 和 SSR 的关系。

SSR 之所以能够实现,本质上是因为虚拟 DOM 的存在

上面我们说过,SSR 的工程中,React 代码会在客户端和服务器端各执行一次。你可能会想,这没什么问题,都是 JavaScript 代码,既可以在浏览器上运行,又可以在 Node 环境下运行。但事实并非如此,如果你的 React 代码里,存在直接操作 DOM 的代码,那么就无法实现 SSR 这种技术了,因为在 Node 环境下,是没有 DOM 这个概念存在的,所以这些代码在 Node 环境下是会报错的。

好在 React 框架中引入了一个概念叫做虚拟 DOM,虚拟 DOM 是真实 DOM 的一个 JavaScript 对象映射,React 在做页面操作时,实际上不是直接操作 DOM,而是操作虚拟 DOM,也就是操作普通的 JavaScript 对象,这就使得 SSR 成为了可能。在服务器,我可以操作 JavaScript 对象,判断环境是服务器环境,我们把虚拟 DOM 映射成字符串输出;在客户端,我也可以操作 JavaScript 对象,判断环境是客户端环境,我就直接将虚拟 DOM 映射成真实 DOM,完成页面挂载。

其他的一些框架,比如 Vue,它能够实现 SSR 也是因为引入了和 React 中一样的虚拟 DOM 技术。

好,接下来我们回过头看流程图,前两步不说了,服务器端渲染肯定要先向 Node 服务器发送请求。重点是第 3 步,大家可以看到,服务器端要根据请求的地址,判断要展示什么样的页面了,这一步叫做服务器端路由。

我们再看第 10 步,当客户端接收到 JavaScript 文件后,要根据当前的路径,在浏览器上再判断当前要展示的组件,重新进行一次客户端渲染,这个时候,还要经历一次客户端路由(前端路由)。

那么,我们下面要说的就是服务器端路由和客户端路由的区别。

SSR 中客户端渲染与服务器端渲染路由代码的差异

实现 React 的 SSR 架构,我们需要让相同的 React 代码在客户端和服务器端各执行一次。大家注意,这里说的相同的 React 代码,指的是我们写的各种组件代码,所以在同构中,只有组件的代码是可以公用的,而路由这样的代码是没有办法公用的,大家思考下这是为什么呢?其实原因很简单,在服务器端需要通过请求路径,找到路由组件,而在客户端需通过浏览器中的网址,找到路由组件,是完全不同的两套机制,所以这部分代码是肯定无法公用。我们来看看在 SSR 中,前后端路由的实现代码:

客户端路由:

const App = () => {
  return (
    <Provider store={store}>
      <BrowserRouter>
        <div>
          <Route path='/' component={Home}>
          </div>
      </BrowserRouter>
    </Provider>
  )
}

ReactDom.render(<App/>, document.querySelector('#root'))

客户端路由代码非常简单,大家一定很熟悉,BrowserRouter 会自动从浏览器地址中,匹配对应的路由组件显示出来。

服务器端路由代码:

const App = () => {
  return 
    <Provider store={store}>
      <StaticRouter location={req.path} context={context}>
        <div>
          <Route path='/' component={Home}>
        </div>
      </StaticRouter>
    </Provider>
}

Return ReactDom.renderToString(<App/>)

服务器端路由代码相对要复杂一点,需要你把 location(当前请求路径)传递给 StaticRouter 组件,这样 StaticRouter 才能根据路径分析出当前所需要的组件是谁。(PS:StaticRouter 是 React-Router 针对服务器端渲染专门提供的一个路由组件。)

通过 BrowserRouter 我们能够匹配到浏览器即将显示的路由组件,对浏览器来说,我们需要把组件转化成 DOM,所以需要我们使用 ReactDom.render 方法来进行 DOM 的挂载。而 StaticRouter 能够在服务器端匹配到将要显示的组件,对服务器端来说,我们要把组件转化成字符串,这时我们只需要调用 ReactDom 提供的 renderToString 方法,就可以得到 App 组件对应的 HTML 字符串。

对于一个 React 应用来说,路由一般是整个程序的执行入口。在 SSR 中,服务器端的路由和客户端的路由不一样,也就意味着服务器端的入口代码和客户端的入口代码是不同的。

我们知道, React 代码是要通过 Webpack 打包之后才能运行的,也就是第 3 步和第10 步运行的代码,实际上是源代码打包过后生成的代码。上面也说到,服务器端和客户端渲染中的代码,只有一部分一致,其余是有区别的。所以,针对代码运行环境的不同,要进行有区别的 Webpack 打包。

服务器端代码和客户端代码的打包差异

简单写两个 Webpack 配置文件作为 DEMO:

客户端 Webpack 配置

{
  entry: './src/client/index.js',
  output: {
    filename: 'index.js',
    path: path.resolve(__dirname, 'public')
  },
  module: {
    rules: [{
      test: /\.js?$/,
      loader: 'babel-loader'
    },{
      test: /\.css?$/,
      use: ['style-loader', {
        loader: 'css-loader',
        options: {modules: true}
      }]
    },{
      test: /\.(png|jpeg|jpg|gif|svg)?$/,
      loader: 'url-loader',
      options: {
        limit: 8000,
        publicPath: '/'
      }
    }]
  }
}

服务器端 Webpack 配置:

{
  target: 'node',
  entry: './src/server/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'build')
  },
  externals: [nodeExternals()],
  module: {
    rules: [{
      test: /\.js?$/,
      loader: 'babel-loader'
    },{
      test: /\.css?$/,
      use: ['isomorphic-style-loader', {
        loader: 'css-loader',
        options: {modules: true}
      }]
    },{
      test: /\.(png|jpeg|jpg|gif|svg)?$/,
      loader: 'url-loader',
      options: {
        limit: 8000,
        outputPath: '../public/',
        publicPath: '/'
      }
    }]
  }
};

上面我们说了,在 SSR 中,服务器端渲染的代码和客户端的代码的入口路由代码是有差异的,所以在 Webpack 中,Entry 的配置首先肯定是不同的。

在服务器端运行的代码,有时我们需要引入 Node 中的一些核心模块,我们需要 Webpack 做打包的时候能够识别出类似的核心模块,一旦发现是核心模块,不必把模块的代码合并到最终生成的代码中,解决这个问题的方法非常简单,在服务器端的 Webpack配置中,你只要加入 target: node 这个配置即可。

服务器端渲染的代码,如果加载第三方模块,这些第三方模块也是不需要被打包到最终的源码中的,因为 Node 环境下通过 NPM 已经安装了这些包,直接引用就可以,不需要额外再打包到代码里。为了解决这个问题,我们可以使用 webpack-node-externals 这个插件,代码中的 nodeExternals 指的就是这个插件,通过这个插件,我们就能解决这个问题。关于 Node 这里的打包问题,可能看起来有些抽象,不是很明白的同学可以仔细读一下 webpack-node-externals 相关的文章或文档,你就能很好的明白这里存在的问题了。

接下来我们继续分析,当我们的 React 代码中引入了一些 CSS 样式代码时,服务器端打包的过程会处理一遍 CSS,而客户端又会处理一遍。查看配置,我们可以看到,服务器端打包时我们用了 isomorphic-style-loader,它处理 CSS 的时候,只在对应的 DOM 元素上生成 class 类名,然后返回生成的 CSS 样式代码。

而在客户端代码打包配置中,我们使用了 css-loader 和 style-loader,css-loader 不但会在 DOM 上生成 class 类名,解析好的 CSS 代码,还会通过 style-loader 把代码挂载到页面上。不过这么做,由于页面上的样式实际上最终是由客户端渲染时添加上的,所以页面可能会存在一开始没有样式的情况,为了解决这个问题, 我们可以在服务器端渲染时,拿到 isomorphic-style-loader 返回的样式代码,然后以字符串的形式添加到服务器端渲染的 HTML 之中。

而对于图片等类型的文件引入,url-loader 也会在服务器端代码和客户端代码打包的过程中分别进行打包,这里,我偷了一个懒,无论服务器端打包还是客户端打包,我都让打包生成的文件存储在 public 目录下,这样,虽然文件会打包出来两遍,但是后打包出来的文件会覆盖之前的文件,所以看起来还是只有一份文件。

当然,这样做的性能和优雅性并不高,只是给大家提供一个小的思路,如果想进行优化,你可以让图片的打包只进行一次,借助一些 Webpack 的插件,实现这个也并非难事,你甚至可以自己也写一个 loader,来解决这样的问题。

如果你的 React 应用中没有异步数据的获取,单纯的做一些静态内容展示,经过上面的配置,你会发现一个简单的 SSR 应用很快的就可以被实现出来了。但是,真正的一个 React 项目中,我们肯定要有异步数据的获取,绝大多数情况下,我们还要使用 Redux 管理数据。而如果想在 SSR 应用中实现,就不是这么简单了。

SSR 中异步数据的获取 + Redux 的使用

客户端渲染中,异步数据结合 Redux 的使用方式遵循下面的流程(对应图中第 12 步):

  1. 创建 Store
  2. 根据路由显示组件
  3. 派发 Action 获取数据
  4. 更新 Store 中的数据
  5. 组件 Rerender

而在服务器端,页面一旦确定内容,就没有办法 Rerender 了,这就要求组件显示的时候,就要把 Store 的数据都准备好,所以服务器端异步数据结合 Redux 的使用方式,流程是下面的样子(对应图中第 4 步):

  1. 创建 Store
  2. 根据路由分析 Store 中需要的数据
  3. 派发 Action 获取数据
  4. 更新Store 中的数据
  5. 结合数据和组件生成 HTML,一次性返回

下面,我们分析下服务器端渲染这部分的流程:

  1. 创建 Store:这一部分有坑,要注意避免,大家知道,客户端渲染中,用户的浏览器中永远只存在一个 Store,所以代码上你可以这么写:
const store = createStore(reducer, defaultState)
export default store;

然而在服务器端,这么写就有问题了,因为服务器端的 Store 是所有用户都要用的,如果像上面这样构建 Store,Store 变成了一个单例,所有用户共享 Store,显然就有问题了。所以在服务器端渲染中,Store 的创建应该像下面这样,返回一个函数,每个用户访问的时候,这个函数重新执行,为每个用户提供一个独立的 Store:

const getStore = (req) => {
  return createStore(reducer, defaultState);
}
export default getStore;
  1. 根据路由分析 Store 中需要的数据: 要想实现这个步骤,在服务器端,首先我们要分析当前出路由要加载的所有组件,这个时候我们可以借助一些第三方的包,比如说 react-router-config, 具体这个包怎么使用,不做过多说明,大家可以查看文档,使用这个包,传入服务器请求路径,它就会帮助你分析出这个路径下要展示的所有组件。
  2. 派发 Action 获取数据: 接下来,我们在每个组件上增加一个获取数据的方法:
Home.loadData = (store) => {
  return store.dispatch(getHomeList())
}

这个方法需要你把服务器端渲染的 Store 传递进来,它的作用就是帮助服务器端的 Store 获取到这个组件所需的数据。 所以,组件上有了这样的方法,同时我们也有当前路由所需要的所有组件,依次调用各个组件上的 loadData 方法,就能够获取到路由所需的所有数据内容了。

  1. 更新 Store 中的数据: 其实,当我们执行第三步的时候,已经在更新 Store 中的数据了,但是,我们要在生成 HTML 之前,保证所有的数据都获取完毕,这怎么处理呢?
// matchedRoutes 是当前路由对应的所有需要显示的组件集合
matchedRoutes.forEach(item => {
  if (item.route.loadData) {
    const promise = new Promise((resolve, reject) => {
      item.route.loadData(store).then(resolve).catch(resolve);
    })
    promises.push(promise);
  }
})

Promise.all(promises).then(() => {
  // 生成 HTML 逻辑
})

这里,我们使用 Promise 来解决这个问题,我们构建一个 Promise 队列,等待所有的 Promise 都执行结束后,也就是所有 store.dispatch 都执行完毕后,再去生成 HTML。这样的话,我们就实现了结合 Redux 的 SSR 流程。

在上面,我们说到,服务器端渲染时,页面的数据是通过 loadData 函数来获取的。而在客户端,数据获取依然要做,因为如果这个页面是你访问的第一个页面,那么你看到的内容是服务器端渲染出来的,但是如果经过 react-router 路由跳转道第二个页面,那么这个页面就完全是客户端渲染出来的了,所以客户端也要去拿数据。

在客户端获取数据,使用的是我们最习惯的方式,通过 componentDidMount 进行数据的获取。这里要注意的是,componentDidMount 只在客户端才会执行,在服务器端这个生命周期函数是不会执行的。所以我们不必担心 componentDidMount 和 loadData 会有冲突,放心使用即可。这也是为什么数据的获取应该放到 componentDidMount 这个生命周期函数中而不是 componentWillMount 中的原因,可以避免服务器端获取数据和客户端获取数据的冲突。

Node 只是一个中间层

上一部分我们说到了获取数据的问题,在 SSR 架构中,一般 Node 只是一个中间层,用来做 React 代码的服务器端渲染,而 Node 需要的数据通常由 API 服务器单独提供。

这样做一是为了工程解耦,二也是为了规避 Node 服务器的一些计算性能问题。

请大家关注图中的第 4 步和第 12,13 步,我们接下来分析这几个步骤。

服务器端渲染时,直接请求 API 服务器的接口获取数据没有任何问题。但是在客户端,就有可能存在跨域的问题了,所以,这个时候,我们需要在服务器端搭建 Proxy 代理功能,客户端不直接请求 API 服务器,而是请求 Node 服务器,经过代理转发,拿到 API 服务器的数据。

这里你可以通过 express-http-proxy 这样的工具帮助你快速搭建 Proxy 代理功能,但是记得配置的时候,要让代理服务器不仅仅帮你转发请求,还要把 cookie 携带上,这样才不会有权限校验上的一些问题。

// Node 代理功能实现代码
app.use('/api', proxy('http://apiServer.com', {
  proxyReqPathResolver: function (req) {
    return '/ssr' + req.url;
  }
}));

总结:

到这里,整个 SSR 的流程体系中关键知识点的原理就串联起来了,如果你之前适用过 SSR 框架,那么这些知识点的整理我相信可以从原理层面很好的帮助到你。

当然,我也考虑到阅读本篇文章的同学可能有很大一部分对 SSR 的基础知识非常有限,看了文章可能会云里雾里,这里为了帮助这些同学,我编写了一个非常简单的 SSR 框架,代码放在这里:

https://files.alicdn.com/tpss...

初学者结合上面的流程图,一步步梳理流程图中的逻辑,梳理结束后,回来再看一遍这篇文章,相信大家就豁然开朗了。

当然在真正实现 SSR 架构的过程中,难点有时不是实现的思路,而是细节的处理。比如说如何针对不同页面设置不同的 title 和 description 来提升 SEO 效果,这时候,我们其实可以用 react-helmet 这样的工具帮我们达成目标,这个工具对客户端和服务器端渲染的效果都很棒,值得推荐。还有一些诸如工程目录的设计,404,301 重定向情况的处理等等,不过这些问题,我们只需要在实践中遇到的时候逐个攻破就可以了。

好了,关于 SSR 的全部分享就到这里,希望这篇文章能够或多或少帮助到你。

参考文档

文章可随意转载,但请保留此 原文链接
非常欢迎有激情的你加入 ES2049 Studio,简历请发送至 caijun.hcj(at)alibaba-inc.com 。
查看原文

赞 157 收藏 109 评论 8

acfasj 关注了问题 · 2019-04-26

vue-cli2打包后css部分样式错乱

使用vue-cli生成的项目。之前是webpack1.12.x的,现在升级了webpack3.6.0。vue(v2.5.2)。部分css样式错乱,检查后发现是css样式被覆盖了,加载顺序出现了问题。npm run dev,生产环境没发现这个问题。webpack1.12.x也没这个问题。

不知道是否是这个插件问题,ExtractTextPlugin。或者是我引用了bootstrap和element-ui有冲突。之前项目是用的bootstrap,升级后才换的element.
ps:webpack3.6.0的配置都是官方的模板,自己没动什么其他地方。
需要哪部分代码可以提问告知。?

new ExtractTextPlugin({
      filename: utils.assetsPath('css/[name].[contenthash].css'),
      // Setting the following option to `false` will not extract CSS from codesplit chunks.
      // Their CSS will instead be inserted dynamically with style-loader when the codesplit chunk has been loaded by webpack.
      // It's currently set to `true` because we are seeing that sourcemaps are included in the codesplit bundle as well when it's `false`, 
      // increasing file size: https://github.com/vuejs-templates/webpack/issues/1110
      allChunks: true,
    }),

main.js

//全局插件css
import './assets/css/animate.min.css' //全局的动画使用
import './assets/css/themify-icons.css' //theimfy字体图标
import './assets/css/icomoon-icons.css' //自定义字体图标
import './assets/css/icomoon2.css'  //二期自定义图标
import 'element-ui/lib/theme-chalk/index.css' //element样式单独引入
import './assets/css/element-variables.scss' //自定义主题颜色等
import '../node_modules/bootstrap/dist/css/bootstrap.min.css'
// import './assets/css/sweet-alert.css' //弹窗样式,如果用ele,考虑后期干掉
import './assets/css/jquery.steps.css' //有步骤时候的css,如入库质检
import './assets/css/first-layout.css' //整个框架的css,如侧边导航
import './assets/css/common.css' //全局公共样式
import './assets/css/reset-element.css'; //重置element-ui的css
//全局插件js
import $ from 'jquery'
import '../node_modules/bootstrap/dist/js/bootstrap.min.js'
import './assets/js/animo.min.js' //animate动画管理
import './assets/js/first-layout.js' //整个框架的js,如侧边导航
import './assets/js/jquery.qrcode.min.js' //条码二维码
import './assets/js/JsBarcode.code39.min.js' //条码二维码
import './assets/js/particles.js'; //首页的背景动画
import './assets/js/jquery.table2excel.js' // 导出为excle相关插件
//原型
import './golbal/vue_prototype.js'

// 百度地图
import BaiduMap from 'vue-baidu-map'
Vue.use(BaiduMap, {
    // ak 是在百度地图开发者平台申请的密钥 详见 http://lbsyun.baidu.com/apiconsole/key */
    ak: 'xxxxxxxx'
})

//element-ui
import ElementUI from 'element-ui'
Vue.use(ElementUI); //elment-ui

//vue相关插件
import Vue from 'vue'
import App from './App'

import router from './router' //引用路由
import store from './store/'


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

关注 3 回答 2

acfasj 提出了问题 · 2019-04-17

mysql 无限极分类, cte递归查询

首先, 我对数据库没有什么有效的了解
我想在mysql里实现一个分类表, 就像京东淘宝的商品分类, 可以实现无限极分类
需求是:

  • 能直接查询某个节点的所有子节点 (比如要查询某个大类下的所有商品)
  • 能查询两个节点间的路径 (要做面包屑导航)

那如何进行建表? 我进行了搜索
What are the options for storing hierarchical data in a relational database?
这个答案说了很多选择, 我选择了第一种Adjacency List, 即邻接表(其他的搞不懂, 都需要维护额外的字段或者表, 我希望表结构尽可能简单)

同时, 根据
MySQL 8.0 Labs: [Recursive] Common Table Expressions in MySQL (CTEs)
cte递归查询可以满足我的需求

所以我使用了 mysql8 和 cte 来做这件事情
表结构就只有 id, parent_id, name

目前表里有200多个分类, 估计最多不会超过一万条
但是我并不知道 cte 递归查询, 会不会有什么性能上的瓶颈 (对于数据量和请求量上来以后)
所以求建议, 我是维持现有的表结构用 cte 递归就好了, 还是需要修改表结构以便于更快速地读?

关注 3 回答 2