2020年1月

问题描述

输入一个字符串,输出去重的全排列(如果有重复的)。这意味着,您必须按所有可能的顺序对输入中的所有字母进行排列。

说明范例

permutations('a'); // ['a']
permutations('ab'); // ['ab', 'ba']
permutations('aabb'); // ['aabb', 'abab', 'abba', 'baab', 'baba', 'bbaa']

分析

所谓排列,就是指从给定个数的元素中取出指定个数的元素进行排序,本题目相当于全排列。两个元素的全排列$$A_2^2$$=$$2!$$=2,即[i,j]的全排列为[ij,ji]。那么如果是$$A_3^3$$=$$3!$$=6是怎么样的过程,假设有[a,b,c],先选择a作为第一个元素,剩下[b,c]相当于一个$$A_2^2$$,接下来选b作为第一个元素,剩下[a,c]相当于一个$$A_2^2$$,以此类推。

求无重复元素集合的排列数量

假设没有重复元素的情况,等价子问题应当是:对子串中每个元素做首元素的情况,进行其他元素的全排列。因此就有以下的实现:

/**
 * idea1 先处理没有重复字母的情况
 * 注意:这里为了方便处理,str其实是str.split("")
 * 如果str不包含重复字母,数量关系上应该是:permutations(str) = str.length * permutations(str*)
 * 理论上递归的终止条件是str.length === 1,此时应当返回1
 * @param str
 */
function permutations(str) {
  if (str.length === 1) {
    return 1;
  }
  let result = 0;
  // 理论上求str组合数,要讨论str每一个元素做起始元素的情况,所以循环str是一定的
  for (let i = 0; i < str.length; i++) {
    let newArr = [...str];
    newArr.splice(i, 1);
    result += permutations(newArr);
  }

  return result;
}

其实“每个元素做首元素”的方式有很多,上述代码中通过创建元素组拷贝,从中splice掉作为首元素的元素,剩下的数列进行递归。这个过程或产生临时数组,试图先对这个问题进行一些优化。

不产生临时变量就要求我们在原数组上进行操作,将str[i]提到str[0],原来的str[0]到str[i-1]的元素分别后移,这种情况需要一个临时变量来进行移动。但是实际上我们并不需要str[0]到str[i-1]的元素都移动,因为我们是求全排列,输入的集合顺序并不产生影响。那么我们只需要将str[0]和str[i]进行对换,这时也只需要一个临时变量。

解决了做首元素的问题,那么如何解决对剩余元素进行递归呢?我们不适用临时数组后,递归调用总是使用原数组,那么当前调用总是针对数组的“剩余部分”--str[step]-str[str.length-1],因为str[0]-str[step-1]总是我们已经确定的序列,所以我们做一个标记,来让当前调用知道递归到了什么程度。

因此我们有以下实现:

function swap(str, a, b) {
  let temp = str[a];
  str[a] = str[b];
  str[b] = temp;
}

/**
 * idea2 在idea1的前提下,优化临时数组的问题
 * @param str
 * @param step 标记递归到了哪一步,默认是0
 */
function permutations2(str, step = 0) {
  if (str.length === step + 1) {
    // console.log(str.join(""))
    return 1;
  }
  let result = 0;
  // 本次调用针对是的str[step]到str[str.length-1]的子串
  for (let i = step; i < str.length; i++) {
    // 对换来得到除了首字母以外的剩余部分
    swap(str, step, i );
    // 针对剩下的部分
    result += permutations2(str, step + 1);
  }

  return result;
}

由于我们还没有解决存在重复字母的问题,permutations2(['a','b','c'])=6,permutations2(['a','b','c','d'])=24,permutations2(['a','b','c','d','e'])=120。看起求全排列的数量似乎并没有问题,但是我们如果在递归结束条件时,输出此时的排列序列,就会发现我们的全排列是有重复的。

我们来一步一步的分析一下。输入str=['a','b','c','d']:

no-swap-again.jpg

求无重复元素集合的具体排列

其实很容易发现问题,step=1的第一阶段递归,第一轮循环针对的是[b,c,d]这个子序列,而第二轮循环变成了[d,b,c],虽然元素没有变(无序集合中元素是相同的),但是顺序改变了。如果把每次递归针对的子序列看做无序集合,那么排列数是不会错的;如果把每次递归针对的子序列看做是有序集合,那么排序就会有问题。如果不需要输出具体的排列情况,理论上这个算法就没问题,但是如果需要输出具体的排列情况,我们需要把swap这个过程,在递归完成后逆转回来,保证每一轮循环起始的有序集合不发生变化。

每一次递归都会对换一次元素,当递归达到终止条件时,正好得到一个排列,我们将其保存在一个数组中,这个数组随着递归收集所有的结果,最终返回。既能查看所有的排列,数组的长度表达排列数量,于是有了下面的代码:

function swap(str, a, b) {
  let temp = str[a];
  str[a] = str[b];
  str[b] = temp;
}

/**
 * idea2 在idea1的前提下,优化临时数组的问题
 * @param str
 * @param step 标记递归到了哪一步,默认是0
 * @param result 保存结果的数组
 */
function permutations2(str, step = 0, result = []) {
  if (str.length === step + 1) {
    result.push(str.join(""));
    return;
  }
  // 本次调用针对是的str[step]到str[str.length-1]的子串
  for (let i = step; i < str.length; i++) {
    // 对换来得到除了首字母以外的剩余部分
    swap(str, step, i );
    // 针对剩下的部分
    permutations2(str, step + 1, result);
    swap(str, step, i );
  }
  return result
}

求任意集合的具体排列

最后我们来解决存在重复元素的情况。我们假设重复的元素是$$str[m]$$和$$str[n]$$,显然$$m \neq n$$,令$$m<n$$。在这个前提下,我们进行到p阶段,判断swap(p,i)是否在将来会遇到等价的情况,如果遇到则跳过。

当p===i时,原始序列肯定不能跳过;

当p<i时,

​ 如果str[p]===str[i]应当跳过,因为至少是和swap(p,p)是等价的。

​ 如果str[p]!==str[i],则需要从i+1开始寻找q使str[q]===str[i],如果有则跳过。

那么可以得到以下判断函数:

function isEqual(arr, step, i) {
  // 自我对换不处理
  if (step === i) {
    return false
  }
  // 至少和str[step]的自我对换是等价的
  if (arr[step] === arr[i]) {
    return true;
  }
  // 从i+1开始寻找,如果有和str[i],则把交换过程放到swap(step,k)
  for (let k = i + 1; k < arr.length; k++) {
    if (arr[k] === arr[i]) {
      return true;
    }
  }
  return false;
}

除了这种判断方式,在众多的代码中还可以看到这样的判断方式:

function isEqual2(arr, step, i) {
  for (let p = step; p < i; p++) {
    if (arr[p] === arr[i]) {
      return true;
    }
  }
  return false;
}

核心是从step遍历到i-1,看看是否存在和str[i]相同的元素。step和i相等时,无法构成循环,所以始终为不相同;step和i不相同时,在step到i-1的范围内查找是否有与str[i]相同的元素,有的话就认为这个对换已经被使用过了。其实swap(step,i)的根本含义是用str[i]做step阶段子序列新的首元素,那么只用判断str[i]是否已经在当前子序列靠前的位置中出现过。

对比来看,isEqual考量的是str[i]如果还有机会当首元素,就等下一次机会吧。当step和i相等时,后面有重复的元素,导致认为还有机会,实际上自对换只有一次。如果step和i不相等,str[step]和str[i]相等,但之后并没有重复的元素,所以认为这样的对换之后不会发生,之后是不会发生,但是其本身就是无效对换。因此我们补充了这两种特殊情况完成判断。

总结

排列问题应该是算法的基础题,很容易发现重复子问题,但是去重的思想有多种,且要考虑的情况比较多,也就有了复杂的判断isEqual和简单一些的判断isEqual2。求排列数是一道典型的递归型算法,使用递归就要想好递归的终止条件,且这个问题能够分切成等价的一个个子问题(求子序列的全排列数)。本文从求排列数开始,到求排列结果,到求去重的排列结果,每一步我都有我的理解方式,也许笨重,也许绕了弯子,但是既然掌握的不扎实,就从最笨的方法一点点掌握,希望我能慢慢熟练掌握这样的思考。

前言

有啥前言啊,突如其来的两个电面,打出了我最真实的水平。废话不多说了。。。

整体内容偏向于前端基础,广度感人,面试官非常nice,并没有到了中途感觉自己是个废物,然后直接挂掉电话去角落哭。整个过程三十分钟,我把我记下来的问题,和我当时的心路历程和回答抛出来,希望能够记录当时的自己有多傻。当然了,总结经验和教训才是这篇博客的主旨,问题驱动的学习看起来功利,多少也是收获。

Q1:聊聊你对于Redux和Vuex的使用经验及看法

我的回答:redux和vuex的功能都是提供统一的状态管理,用法上都是针对组件进行需要的数据注入,vuex的mapState,redux的connect。vuex的状态树核心还是一个vue实例,state对应到data,getter对应到computed。redux的话没有再深入了解。

写在这里:我是一个主要使用vue的同学,vuex的源码也看过一些,反正知道的都说了。react算是自己想了解,redux也只是停留在了使用的层面。所以说用不用在生产上可能不重要,有没有自己写出来的项目也不那么重要,重要的是了解一些原理,谁知道啥时候就用上了。

A1:对比分析法

先允许我摘抄一段尤大在知乎上的回答:

Vuex 其实是一个针对 Vue 特化的 Flux,主要是为了配合 Vue 本身的响应式机制。当然吸取了一些 Redux 的特点,比如单状态树和便于测试和热重载的 API,但是也选择性的放弃了一些在 Vue 的场景下并不契合的特性,比如强制的 immutability(在保证了每一次状态变化都能追踪的情况下强制的 immutability 带来的收益就很有限了)、为了同构而设计得较为繁琐的 API、必须依赖第三方库才能相对高效率地获得状态树的局部状态等等(相比之下 Vuex 直接用 Vue 本身的计算属性就可以)

所以 Vue + Vuex 会更简洁,也不需要考虑性能问题,代价就是 Vuex 只能和 Vue 配合。Vue + Redux 也不是不可以,但是 Redux 作为一个泛用的实现和 Vue 的契合度肯定不如 Vuex。

从这一段我们可以抽取出一些比较高级别的观点:

  • Vuex相对于Redux的共性:单一状态树,便于测试和热重载的API。
  • Vuex相对于Redux的差异:没有强制的immutability,没有复杂的API,不依赖第三方库就可以获取局部状态(这里应该说的是Redux只提供了一个getState方法获取整个状态树,而Vuex通过内涵的Vue实例的computed就可以实现)。
  • Vuex只能配合Vue,而“Redux is a predictable state container for JavaScript apps.”

从两者的工作流程上来对比:

  • Vuex:视图层dispatch-actions或commit-mutations,mutate-state,state-render
  • Redux:视图层dispatch-actions,reducer消费actions并返回新的store-render

后续的任务肯定是再深入的了解Redux乃至Flux的工作原理。Vuex的源码倒是读过一些,整理好了也会发出来。

Q2:Vue是如何实现响应式的,收集依赖是怎么做的,computed是怎么实现

我的回答:Vue的响应式核心是Object.defineProperty,通过对响应式数据设置getter和setter,并在编译模板和处理computed属性时收集依赖,作为数据的订阅者,setter方法被触发时,能够主动通知到这些依赖使其产生响应。编译模板时,对于v-model指令指明的数据以来关系,则会产生一个数据与虚拟DOM之间的依赖,数据变更时,会通过patch的过程,将响应产生变化的虚拟DOM渲染到页面上。computed答得烂七八杂我也不记得。

A2:先熟悉官方文档中“深入响应式原理”一章

首先,我们在编写组件时,统一导出的是一个普通的对象,解析.vue文件时,编译模板,基于这个对象创建Vue实例。对于data属性,通常是个函数,返回实例所需的状态集合,遍历这个集合使用Object.defineProperty把这些状态转化为getter/setter。官方文档也提示了,Object.defineProperty不可以被转化到ES5,所以IE8无法支持。

对组件内的状态,也就是data赋值会触发setter方法,而读取则会触发getter方法,这个过程是隐含的。而响应式则通过watcher实例实现,组件渲染过程中肯定会访问data,这就会触发getter,从而触发watcher收集到一个依赖关系,而后当被依赖的数据被重新赋值,触发setter,就会通知watcher,按照之前收集的依赖关系进行通知,触发组件的render函数。我们编写的模板部分,就相当于render函数。

由于创建getter/setter是在创建Vue实例的阶段执行的,如果对实例追加属性,无法使其正确的产生getter/setter,也不会让watcher感知到它与其他数据的关联,可以使用Vue.set,或者Vue.prototype.$set来实现。

异步更新队列可能也会被问到,同一个EventLoop,或者说某一段不包含一部代码的逻辑,可能会有多个数据更新,多次触发watcher,这些更改会被推出一个队列,这个队列中的更改会进行去重,并尝试使用Promise.then、MutationObserver、setImmediate甚至setTimeout(fn,0)来代替。注意这些方法有些是宏任务,有些是微任务,但是都是为了实现Vue渲染的一个“tick”。所以Vue提供了nextTick函数来将修改推入下一个更新队列,防止更新被合并。

注意:不可被监听的变化包括1.数组中原始类型元素的值的变化。2.数组的length变化。这里解释一下为什么,defineProperty这个API,看名字就知道是给某个对象定义一个属性,而属性其实是包含元属性的,就是描述这个属性的属性。就用数组的length属性,这个属性具有什么特性呢--writable:true可写、enumerable:false不可被枚举、configurable:false不可被配置。对于[[Configurable]]的描述如下:

If false, the property can't be deleted, can't be changed to an accessor property and attributes other than [[Value]] and [[Writable]] can't be changed.

不能被delete操作符删除,不能被变更为访问器属性,[[Value]]和[[Writable]]的值也不能修改。所以length不能转换成getter/setter,也就没法收集依赖和通知watcher。

至于computed属性,通常我们只会用到getter,其实也可以设置它的setter,比如:

{
  data: {
    firstName: 'Foo',
    lastName: 'Bar'
  },
  computed: {
    fullName: {
      get(){
         return this.firstName + ' ' + this.lastName
      },
      set(newValue): {
        const [firstName, lastName] = newValue.split(" ");
        this.firstName = firstName;
        this.lastName = lastName;
      }
    }
  }
}

这样看起来更像是一个看得到getter/setter的可响应的一个属性,而相比data中的属性,computed的初始值往往不是确定的,而是依赖关系得到的,而这个初始值,或者说变化后的值,都是延迟求值,用到时才会调用get来更新一下自己。且具有缓存性。

这里先简单聊一下computed的创建与工作方式,稍后我们再来通过源码来详细聊聊。

Vue的响应式离不开Watcher、Observe和Dep。初始化computed时,会相应的对每一个computed字段创建一个内部watcher,即_computedWatchers集合。然后通过defineComputed函数,将computed字段转换成访问器属性挂载到Vue实例上。而defineComputed定义的get访问器,会从_computedWatchers中拿出对应的watcher,如果有依赖则调用evaluate方法进行懒求值,然后返回计算结果。更多的细节,稍后会更新一篇Vue响应式原理源码分析的文章,我相信会在Watcher的部分看到端倪。

Q3:Vue3相比Vue2做出了哪些改变,Proxy有什么优势

我的回答:根据我看的新闻和技术分享,Vue3中最主要的变化就是使用Proxy取代Object.defineProperty作为响应式的底层技术实现。其他的我也不熟悉,怕说了被深究,就说了这一点。Proxy的优势能够监听更多的变化,具体有哪些我记不得了。真是拉胯,知道Proxy至少证明我还是看过技术新闻的。

A3:回顾尤大的发布会PPT

Vue3就目前公开的消息来看,最明显的还是Proxy取代Object.defineProperty。除此之外,通过回顾发布会的ppt,我们还应当知道以下特性:

  • 全局API,内置组件和功能支持tree-shaking,也就是说可以部分引用,而不用担心将整个Vue引入
  • 基于Proxy的变动侦测,性能更优
  • 代码采用monorepo,简单来说就是像乐高积木的一样的项目
  • TypeScript
  • 开放更多底层功能,如自定义渲染函数
  • 模板静态分析,生成更优化的VDOM树,静态节点和动态节点分离,更新时直接便利动态节点
  • Function-based API,类似React的函数组件,逻辑复用更容易,相比mixin更明了,更好的类型支持,更好的tree-shaking。mixin使用的属性来源不清晰,被mixin的组件也容易知道如何配合;高阶组件的props来源不明确;composition functions就是简单的函数,让组件以功能层面得到拆解,而不是生命周期。

除了上述内容,更多详情请看:github.com/vuejs/rfcs

下面再来说说Proxy。Proxy是一个完全不支持IE的特性,作用是基于target目标对象,使用handler作为代理行为,创建一个代理实例。使用代理实例访问相应的属性,行为将被handler截获并处理。关于Proxy的详细内容稍后也可以写一篇文章,这里先简单讨论一下。

Proxy的handler可以设置以下属性:

  • handler.getPrototypeOf():拦截Object.getPrototypeOf操作
  • handler.setPrototypeOf():拦截Object.setPrototypeOf操作
  • handler.isExtensible():拦截Object.isExtensible操作
  • handler.preventExtensions():拦截Object.preventExtensions操作
  • handler.getOwnPropertyDescriptor():拦截Object.getOwnPropertyDescriptor操作
  • handler.defineProperty():拦截Object.defineProperty操作
  • handler.has():拦截in操作符
  • handler.get():拦截访问操作
  • handler.set():拦截赋值操作,这里可以参见Reflect.set,对于数组中的元素赋值也可以被拦截,这里是普通setter做不到的。而且数组的length属性,也在set范围内。
  • handler.deleteProperty():拦截delete操作符
  • handler.ownKeys():拦截Object.getOwnPropertyNames和Object.getOwnPropertySymbols
  • handler.apply():拦截函数的调用
  • handler.construct():拦截new操作符

小结

现总结了前三个问题,可以发现对于Vue的响应式原理仍然只是一知半解,没有对于源码进行深入的观察和分析,只能得到诸如Object.defineProperty设置访问器属性、收集依赖、通知变化等零零散散的内容。事实上这里还应该注重一些概念的细化,比如观察者模式和订阅发布模式的区别,Vue的watcher用的是哪一种;Proxy元编程能力到底如何,具体哪些算作set操作,借助其他的拦截功能是不是可以实现更多功能。

除了这些细节和具体的知识点要更进一步的深入了解外,应用层的功能和工作方式的对比,也是学习的一种基本技能。Vuex和Redux都提供状态管理功能,使用上能够根据经验说个一二三点,但是差异性往往蕴含更多内容。Vuex只能结合Vue使用,因为核心的响应式状态树是依赖Vue实例实现的;Redux虽然知道action和reducer,但是对局部状态树的获取,与组件的链接关系还不甚明确。

这些问题都是和两大主流视图渲染库息息相关的问题,同时也生发出了一些基础知识,如设计模式和Proxy等知识点。在使用工具的过程中,多去探求3W--是什么(Proxy是什么)&为什么(为什么要使用状态管理)&如何(视图如何根据data进行变化),除了广度也要有深度,这是我在面试和学习中得到的一条真理。同时面试官也对我提出了很中肯的建议,不管是内建还是外显知识体系,都要有一个清晰的思路,每个问题说明白,有条理,这样才不会错过什么细节。

Next.js项目搭建实录

本文档随手记录基于Next.js项目的搭建过程,融合各个功能的过程中遇到的问题,评估是否可以满足开发需要,同时考虑便利性和稳定性。记录的顺序部分前后。

使用styled-components

Next.js支持css-in-js,但是写法别扭所以暂不考虑,使用React比较流行的styled-components,把HTML标签装饰成组件的形式,复用度高,自带代码分割和前缀补全,对于“主题”支持更好,维护起来也很方便。编程式的样式书写,也有一定的SCSS或less的优势。webstorm安装一下styled-components支持,就可以方便的书写样式表。

npm install –save styled-components

为了支持Next.js的SSR,还要手动创建(如果没有的话).babelrc文件,开启styled-components的ssr支持:

{"presets": ["next/babel"],"plugins": [["styled-components",{"ssr": true}]]}

扩展Next.js的App植入<ThemeProvider>,在这里使用createGlobalStyle创建全局样式:

import App from 'next/app'
import React from 'react'
import {ThemeProvider, createGlobalStyle} from 'styled-components'

const theme = {
  colors: {
    primary: '#0070f3'
  }
};

const GlobalStyle = createGlobalStyle`
  body {
    padding: 0;
    margin: 0;
  }
`;

export default class MyApp extends App {
  render() {
    const {Component, pageProps} = this.props;
    return (
      <ThemeProvider theme={theme}>
        <React.Fragment>
          <GlobalStyle/>
          <Component {...pageProps} />
        </React.Fragment>
      </ThemeProvider>
    )
  }
}

扩展Next.js的Document注入用于加载样式表的head(讲道理这个并没有看懂,但是styled-components的官方文档让我们直接copy这些逻辑,那我就不管了):

import Document from 'next/document'
import {ServerStyleSheet} from 'styled-components'

export default class MyDocument extends Document {
  static async getInitialProps(ctx) {
    const sheet = new ServerStyleSheet();
    const originalRenderPage = ctx.renderPage;

    try {
      ctx.renderPage = () =>
        originalRenderPage({
          enhanceApp: App => props => sheet.collectStyles(<App {...props} />)
        });

      const initialProps = await Document.getInitialProps(ctx);
      return {
        ...initialProps,
        styles: (
          <>
            {initialProps.styles}
            {sheet.getStyleElement()}
          </>
        )
      }
    } finally {
      sheet.seal()
    }
  }
}

使用iconfont

由于iconfont用到了svg和ttf等特殊文件,所以需要一些特殊的帮助。通过查询,我们需要next-font来扩展webpack配置。

const withSass = require('@zeit/next-sass');
const withCSS = require('@zeit/next-css');
const withFonts = require('next-fonts');
module.exports = withFonts(withCSS(withSass({
    enableSvg: true,
    webpack(config, options) {
        return config;
    }
})));

注意处理的顺序,现将sass处理成css,在处理css的时候需要引用字体等,所以顺序是withFonts>withCSS>withSass。并且配置要注意webpack属性,将config返回出来。处理好loader,就可以在_app.tsx中进行全局引用了。

使用路由

Next.js的路由系统基于/pages目录下的文件组织,和请求进行对应,参数路由以[params].js对应。

测试发现:在/pages目录下所有的子目录及组件文件,都会被映射成路由。比如/pages/post/components/Button.js会响应/post/components/Button请求。

/pages目录下所有的非“_”开头的文件,都将按其相对路径被映射成路由,所以页面中包含的组件,需要单独出去进行维护。

使用react-intl国际化

官方范例

基本用法

按照react-intl的要求准备语言文件,要注意的是react-intl使用的messages是一个普通的平铺的对象,但是键可以使用路径形式,即‘common.confirm.ok’。

module.exports = {title: 'Next-Demo 标题',greeting: '欢迎!','common.confirm.ok': '确认'};

因为下面的例子多语言文件是从服务端加载的,所以使用CommonJS形式导出。为了更好的维护多语言文件,可以引用flat库将其平铺。

next-with-react-intl这个例子,主要是在服务端结合了国际化的部分。先来看server.js的部分代码:

server.get('*', (req, res) => {
  const accept = accepts(req);
  const locale = (accept.language(accept.languages(supportedLanguages)) || 'zh-CN').split("-")[0];
  req.locale = locale;
  req.localeDataScript = getLocaleDataScript(locale);
  req.messages = flat(getMessages(locale));
  return handle(req, res);
});
const localeDataCache = new Map();
const getLocaleDataScript = locale => {
  const lang = locale.split('-')[0];
  if (!localeDataCache.has(lang)) {
    const localeDataFile = require.resolve(`@formatjs/intl-relativetimeformat/dist/locale-data/${lang}`);
    const localeDataScript = readFileSync(localeDataFile, 'utf8');
    localeDataCache.set(lang, localeDataScript)
  }
  return localeDataCache.get(lang)
};

理论上服务端跟react-intl没关系,而是跟intl国际化有关系。这里负责解析所有的请求,并通过accepts库,根据请求信息来决定应当使用的语种,然后通过getLocaleDataScript方法,将formatjs中对应语种的通用多语言内容提取出来,再将对应语言的多语言文件内容读取出来,统统塞到req中,这些信息就会被带到浏览器端。下面的工作交给_app.tsx和_document.tsx。

// _app.tsx
export default class MyApp extends App<Props> {
  static getInitialProps = async function ({Component, router, ctx}) {
    let pageProps = {};

    if (Component.getInitialProps) {
      pageProps = await Component.getInitialProps(ctx);
    }

    const {req} = ctx;
    const {locale, messages} = req || (window as any).__NEXT_DATA__.props;
    return {pageProps, locale, messages};
  };


  render() {
    const {Component, pageProps, locale, messages} = this.props;
    return (
      <ThemeProvider theme={theme}>
        <IntlProvider locale={locale} messages={messages}>
          <Component {...pageProps} />
        </IntlProvider>
      </ThemeProvider>
    )
  }
}

扩展App组件,相当于全局处理,所以无论你访问哪个页面,都会先处理这里的逻辑。getInitialProps方法中,从上下文ctx中拿到req,从req中获得我们在后端插入的locale和messages属性,将这两个属性设置给<IntlProvider/>组件。使用多语言如下:

// index.tsx
export default class Index extends React.Component {
  render() {
    return (
        <div>
          <FormattedMessage id="greeting" defaultMessage="拉拉拉"/>
        </div>
    )
  }
}

使用FormattedMessage组件根据id属性来加载多语言内容,defaultMessage用于在找不到对应id的时候默认展示。

切换多语言

使用Redux

官方范例

Redux的基础内容不在这里赘述,主要说明一下和Next.js服务端部分结合使用。组件和服务端沟通的桥梁是getInitialProps方法,所以在这里将状态树构造出来,挂到上下文中。为了实现这个过程,装饰一下_app.tsx

const isServer = typeof window === 'undefined';
const __NEXT_REDUX_STORE__ = '__NEXT_REDUX_STORE__';

function getOrCreateStore(initialState) {
  // 服务端总是新建状态树
  if (isServer) {
    return initializeStore(initialState)
  }

  // 客户端的话就要考虑一下是不是已有全局状态树
  if (!window[__NEXT_REDUX_STORE__]) {
    // 状态树会被挂到window下的全局属性
    window[__NEXT_REDUX_STORE__] = initializeStore(initialState)
  }
  return window[__NEXT_REDUX_STORE__]
}

export default App => {
  return class AppWithRedux extends React.Component {
    static async getInitialProps(appContext) {
      const reduxStore = getOrCreateStore();

      // 服务端初始化的store被挂到上下文中
      appContext.ctx.reduxStore = reduxStore;

      let appProps = {};
      if (typeof App.getInitialProps === 'function') {
        appProps = await App.getInitialProps(appContext)
      }

      return {
        ...appProps,
        initialReduxState: reduxStore.getState()
      }
    }

    constructor(props) {
      super(props);
      // 客户端也去初始化,由于区分了端,这里保证拿到的状态树是统一的
      this.reduxStore = getOrCreateStore(props.initialReduxState)
    }
  }
}

开发便捷性

使用别名

为了编辑器和next都能够正常识别使用别名,我们需要ts、next内置webpack和编辑器支持。ts支持需要在tsconfig.json文件中对compilerOptions属性进行扩展配置,一般来说可以把这部分隔离出来,在tsconfig.json中用extends进行融合:

// path.json
{"compilerOptions": {
    "baseUrl": ".",
    "paths": {
        "@pageComponents/*": ["./components/pages/*"],
    }
}}
// tsconfig.json
{
    "extends": "./paths.json",
    // ... 其他配置内容
}

next内置的webpack支持别名就比较简单了,webpack有对应的设置项:

// next.config.js
const path = require('path');
module.exports = {
    // ...其他next配置
    webpack: (config, {isServer}) => {
        // .. 其他webpack配置
        Object.assign(
            config.resolve.alias, 
            {'@pageComponents': path.resolve(__dirname, 'components/pages')});
        return config
    },
}

使用webstorm编辑器的话,为了能够使用快捷跳转,还要给编辑器提供一个单独的config文件用于创建索引,这个文件应当和普通webpack的配置文件写法相同,只提供alias的部分就行,然后在webstorm的项目配置中选择这个文件即可:

// alias.config.js
const path = require('path');
module.exports = {
    resolve: {
        alias: {
            '@pageComponents': path.resolve(__dirname, 'components/pages')
        }
    }
};