hasaki

hasaki 查看完整档案

杭州编辑  |  填写毕业院校  |  填写所在公司/组织 sosout.com 编辑
编辑

Zant/Zanm框架的作者!Zant/Zanm 是一套基于 Vue.js 的开源 UI 组件库,Zant 主要服务于 PC 界面的中后台产品,Zanm 主要服务于移动端界面的产品。地址:http://www.zantb.com/

个人动态

hasaki 关注了用户 · 2020-10-18

独钓寒江雪 @king_hcj

⚜ 个人博客:https://king-hcj.github.io/
🦅 2021/1/11 孤篷 更名 独钓寒江雪
🐦 思否2019年度有奖征文 文采三剑客
👑 Nothing is given, Everything is earned!

关注 195

hasaki 回答了问题 · 2020-10-14

解决keep-alive 之下新增子路由 不渲染

不多言,上菜。

<template>
  <div id="app">
    <div id="nav">
      <router-link to="/">Home</router-link> |
      <router-link to="/about">About</router-link>
    </div>
    <keep-alive :include="cachedView">
      <router-view v-if="!realodKey" :key="$route.name"></router-view>
    </keep-alive>
    <router-view v-if="realodKey" :key="realodKey"></router-view>
    <div @click="reload">刷新</div>
    <div @click="add">新增子路由</div>
  </div>
</template>

<script>
export default {
  name: "App",
  data() {
    return {
      realodKey: 0,
      cachedView: []
    };
  },
  watch: {
    $route: {
      immediate: true,
      handler() {
        this.realodKey = 0;
        if (!this.cachedView.includes(this.$route.name)) {
          this.cachedView.push(this.$route.name);
        }
      }
    }
  },
  methods: {
    reload() {
      ++this.realodKey;
      this.cachedView = this.cachedView.filter(view => {
        return view !== this.$route.name;
      });
    },
    add() {
      this.$router.push({ path: "/about" });
    }
  }
};
</script>

image.png

关注 6 回答 6

hasaki 关注了问题 · 2020-02-10

webpack配置中,给`test`传入函数时,他的参数是什么

如题,webpack配置中,给test传入函数时,他的参数是什么

关注 1 回答 0

hasaki 发布了文章 · 2018-09-20

分析 HTTP 203 出的一个 JS 题目

后续内容更新,请前往:个人博客,欢迎一起交流。

这是一道出自 HTTP 203 的 JS 题目。HTTP 203 是 Youtube 上的一个栏目,主要讲一些有趣的知识。

原题目是这样的:

for(
  let i = (setTimeout(()=>console.log(i), 2333), 0);
  i < 2;
  i++
) {
  
}

// 问 2333 毫秒之后打印出什么

答案是 2333 毫秒后打印出 0。 为什么呢?

在开始分析题目之前,我们先来回顾几个知识点:

for 语法

for (语句 1; 语句 2; 语句 3) {
    被执行的代码块
}

语句 1(代码块)开始前执行;
语句 2 定义运行循环(代码块)的条件;
语句 3 在循环(代码块)已被执行之后执行;

执行的顺序为:
1.第一次循环,即初始化循环。
首先执行语句1(一般为初始化语句),再执行语句2(一般为条件判断语句),判断语句1是否符合语句2的条件,如果符合,则执行代码块,否则,停止执行,最后执行语句3。
2.其他循环:
首先判断前一次语句3的执行结果是否符合执行语句2的条件,如果符合,继续执行代码块,否则停止执行,最后执行语句3。如此往复,直到前一次语句3的执行结果不满足符合执行语句2的条件。

总的来说,执行顺序是一致的,先执行条件判断(语句2),再执行代码块,最后执行语句3。如此往复,区别在于条件判断的对象,在第一次判断时,是执行语句1,初始化的对象,后续的判断对象是执行语句3的结果。

逗号表达式

逗号表达式,因为原题目中就有使用逗号表达式let i = (setTimeout(()=>console.log(i), 2333), 0);

逗号表达式的一般形式是:表达式1,表达式2,表达式3......表达式n。
逗号表达式的求解过程是:先计算表达式1的值,再计算表达式2的值,......一直计算到表达式n的值。最后整个逗号表达式的值是表达式n的值。 看下面几个例子:

x=8*2, x*4  // 整个表达式的值为64,x的值为16

(x=8*2, x*4), x*2 // 整个表达式的值为32,x的值为16

x=(z=5, 5*2) // 整个表达式为赋值表达式,它的值为10,z的值为5,x的值为10

x=z=5, 5*2 // 整个表达式为逗号表达式,它的值为10,x和z的值都为5

逗号表达式用的地方不太多,一般情况是在给循环变量赋初值时才用得到。所以程序中并不是所有的逗号都要看成逗号运算符,尤其是在函数调用时,各个参数是用逗号隔开的,这时逗号就不是逗号运算符。

基础知识回顾完毕,我们通过几个简单示例一步一步地逼近原题目:

示例一:基础知识 for 循环

for (var i = 0; i < 2; i++) {
    console.log(i);
}

// 打印什么

这个无需多说,答案输出 0 1。

示例二:我们稍微改造下,将 log 放入 setTimeout 中

for (var i = 0; i < 2; i++) {
    setTimeout(() => console.log(i));
}

// 打印什么

答案输出 2 2。分析下:
上述代码中,变量 i 是 var 命令声明的,在全局范围内都有效,所以全局只有一个变量 i。每一次循环,变量 i 的值都会发生改变,而循环内被赋给 setTimeout 内部的 console.log(i),里面的 i 指向的就是全局的 i。也就是说,这里面所有的 i 指向的都是同一个 i,导致运行时输出的是最后一轮的 i 的值,也就是 2。

示例三:我再稍微改造下,将上述 var 改为 let。

for (let i = 0; i < 2; i++) {
    setTimeout(() => console.log(i));
}

// 打印什么

答案输出 0 1。分析下:
上述代码中,变量 i 是 let 声明的,当前的 i 只在本轮循环有效,所以每一次循环的 i 其实都是一个新的变量,所以最后输出的是0 1。你可能会问,如果每一轮循环的变量i都是重新声明的,那它怎么知道上一轮循环的值,从而计算出本轮循环的值?这是因为 JavaScript 引擎内部会记住上一轮循环的值,初始化本轮的变量i时,就在上一轮循环的基础上进行计算。

原题目

for(
  let i = (setTimeout(()=>console.log(i), 2333), 0); // 语句1
  i < 2; // 语句2
  i++ // 语句3
) {
  
}

// 问 2333 毫秒之后打印出什么

答案是 2333 毫秒后打印出 0。分析下:
上述题目中,变量 i 是 let 声明的,当前的 i 只在本轮循环有效,后面的表达式是逗号表达式,取最后一个值,即 i = 0,settimeout 在语句1,由于语句1只在第一次循环执行,因此 settimeout 的作用域是第一次迭代的作用域,且只执行一次。第一次迭代时 i = 0,所以答案是 2333 毫秒后打印出 0。

查看原文

赞 21 收藏 9 评论 0

hasaki 回答了问题 · 2018-09-18

react-router-dom怎么打开一个子组建,而且不让他显示父组件

谢邀!临近下班,回答一发。
如果您希望组件First和组件Son,两个组件内容同时展现一个页面中,去掉 exact 参数 或 将值改为false。如果您不希望两个组件内容同时展现一个页面中,将组件First的<Route path="/son" component={Son}></Route> 挪到main.jsx即可,记得删掉Switch

关注 2 回答 2

hasaki 发布了文章 · 2018-09-14

有史以来最小的编译器源码解析

后续内容更新,请前往:个人博客,欢迎一起交流。

前言

源文件:the-super-tiny-compiler
详细中文注释:the-super-tiny-compiler

稍微接触一点前端,我们都知道现在前端“ES6即正义”,然而浏览器的支持还处于进行阶段,所以我们常常会用一个神奇的工具将 ES6 语法转换为目前支持比较广泛的 ES5 语法,这里我们所说的神奇的工具就是编译器。编译器功能非常纯粹,将字符串形式的输入语言编译成目标语言的代码字符串(以及sourcemap),常用的编译器除了我们熟知的 Babel 之外,还有 gcc。不过我们今天的主角是号称可能是有史以来最小的编译器the-super-tiny-compiler,去掉注释也就200多行代码,作者 James Kyle 更是 Babel 的活跃维护者之一。这个编译器的功能很简单,主要把 Lisp 风格的函数调用转换成 C 风格的,例如:

Lisp 风格(转化前)C 风格(转化后)
2 + 2(add 2 2)add(2, 2)
4 - 2(subtract 4 2)subtract(4, 2)
2 + (4 - 2)(add 2 (subtract 4 2))add(2, subtract(4, 2))

编译器工作的三个阶段

绝大多数编译器的编译过程都差不多,主要分为三个阶段:
解析:将代码字符串解析成抽象语法树。
转换:对抽象语法树进行转换操作。
代码生成:根据转换后的抽象语法树生成目标代码字符串。

解析

解析过程主要分为两部分:词法分析和语法分析。
1、词法分析是由词法分析器把原始代码字符串转换成一系列词法单元(token),词法单元是一个数组,由一系列描述独立语法的对象组成,它们可以是数值、标签、标点符号、运算符、括号等。
2、语法分析是由语法分析器将词法分析器生成的词法单元转化为能够描述语法结构(包括语法成分及其关系)的中间表示形式(Intermediate Representation)或抽象语法树(Abstract Syntax Tree),其中抽象语法树(简称AST)是个深层嵌套的对象。

我们简单看一下 the-super-tiny-compiler 的整个解析过程:

// 原始代码字符串
(add 2 (subtract 4 2))

// 词法分析转化后生成的词法单元
[
  { type: 'paren',  value: '('        },
  { type: 'name',   value: 'add'      },
  { type: 'number', value: '2'        },
  { type: 'paren',  value: '('        },
  { type: 'name',   value: 'subtract' },
  { type: 'number', value: '4'        },
  { type: 'number', value: '2'        },
  { type: 'paren',  value: ')'        },
  { type: 'paren',  value: ')'        },
]

// 语法分析转化后生成的抽象语法树(AST)
{
  type: 'Program',
  body: [{
    type: 'CallExpression',
    name: 'add',
    params: [{
      type: 'NumberLiteral',
      value: '2',
    }, {
      type: 'CallExpression',
      name: 'subtract',
      params: [{
        type: 'NumberLiteral',
        value: '4',
      }, {
        type: 'NumberLiteral',
        value: '2',
      }]
    }]
  }]
}

转换

转换过程主要任务是修改 AST,即遍历解析过程生成的 AST,同时进行一系列操作,比如增/删/改节点、增/删/改属性、创建新树等,我们简单看一下 the-super-tiny-compiler 的整个转换过程:

// 原始代码字符串
(add 2 (subtract 4 2))

// 解析过程生成的 AST
{
  type: 'Program',
  body: [{
    type: 'CallExpression',
    name: 'add',
    params: [{
      type: 'NumberLiteral',
      value: '2',
    }, {
      type: 'CallExpression',
      name: 'subtract',
      params: [{
        type: 'NumberLiteral',
        value: '4',
      }, {
        type: 'NumberLiteral',
        value: '2',
      }]
    }]
  }]
}

// 转换过程生成的 AST
{
  type: 'Program',
  body: [{
    type: 'ExpressionStatement',
    expression: {
      type: 'CallExpression',
      callee: {
        type: 'Identifier',
        name: 'add'
      },
      arguments: [{
        type: 'NumberLiteral',
        value: '2'
      }, {
        type: 'CallExpression',
        callee: {
          type: 'Identifier',
          name: 'subtract'
        },
        arguments: [{
          type: 'NumberLiteral',
          value: '4'
        }, {
          type: 'NumberLiteral',
          value: '2'
        }]
      }
    }
  }]
}

代码生成

根据转换过程生成的抽象语法树生成目标代码字符串。

源码实现

接下来我们根据编译器工作的三个阶段逐一分析一下 the-super-tiny-compiler 源码实现。

词法分析

词法分析器主要任务把原始代码字符串转换成一系列词法单元(token)。

// 词法分析器 参数:代码字符串input
function tokenizer(input) {
  // 当前正在处理的字符索引
  let current = 0;
  // 词法单元数组
  let tokens = [];

  // 遍历字符串,获得词法单元数组
  while (current < input.length) {
    let char = input[current];

    // 匹配左括号
    if (char === '(') {

      // type 为 'paren',value 为左圆括号的对象
      tokens.push({
        type: 'paren',
        value: '('
      });

      // current 自增
      current++;

      // 结束本次循环,进入下一次循环
      continue;
    }

    // 匹配右括号
    if (char === ')') {
      tokens.push({
        type: 'paren',
        value: ')'
      });

      current++;

      continue;
    }

    // \s:匹配任何空白字符,包括空格、制表符、换页符、换行符、垂直制表符等
    let WHITESPACE = /\s/;
    // 跳过空白字符
    if (WHITESPACE.test(char)) {
      current++;
      continue;
    }

    // [0-9]:匹配一个数字字符
    let NUMBERS = /[0-9]/;
    // 匹配数值
    if (NUMBERS.test(char)) {
      let value = '';
      // 匹配连续数字,作为数值
      while (NUMBERS.test(char)) {
        value += char;
        char = input[++current];
      }
      tokens.push({
        type: 'number',
        value
      });

      continue;
    }

    // 匹配形如"abc"的字符串
    if (char === '"') {
      let value = '';

      // 跳跃左双引号
      char = input[++current];

      // 获取两个双引号之间的所有字符
      while (char !== '"') {
        value += char;
        char = input[++current];
      }

      // 跳跃右双引号
      char = input[++current];

      tokens.push({
        type: 'string',
        value
      });

      continue;
    }

    // [a-z]:匹配1个小写字符 i 模式中的字符将同时匹配大小写字母
    let LETTERS = /[a-z]/i;
    // 匹配函数名,要求只含大小写字母
    if (LETTERS.test(char)) {
      let value = '';

      // 获取连续字符
      while (LETTERS.test(char)) {
        value += char;
        char = input[++current];
      }

      tokens.push({
        type: 'name',
        value
      });

      continue;
    }

    // 无法识别的字符,抛出错误提示
    throw new TypeError('I dont know what this character is: ' + char);
  }

  // 词法分析器的最后返回词法单元数组
  return tokens;
}

通过遍历代码字符串,分拣出各个词素,然后构成由一系列描述独立语法的对象组成的数组的词法单元。

语法分析

语法分析器主要任务是将词法分析器生成的词法单元转化为能够描述语法结构(包括语法成分及其关系)的中间表示形式(Intermediate Representation)或抽象语法树(Abstract Syntax Tree)。

// 语法分析器 参数:词法单元数组
function parser(tokens) {
  // 当前正在处理的 token 索引
  let current = 0;
  // 递归遍历(因为函数调用允许嵌套),将 token 转成 AST 节点
  function walk() {
    // 获取当前 token
    let token = tokens[current];

    // 数值
    if (token.type === 'number') {
      // current 自增
      current++;

      // 生成一个 AST节点 'NumberLiteral',用来表示数值字面量
      return {
        type: 'NumberLiteral',
        value: token.value,
      };
    }

    // 字符串
    if (token.type === 'string') {
      current++;

      // 生成一个 AST节点 'StringLiteral',用来表示字符串字面量
      return {
        type: 'StringLiteral',
        value: token.value,
      };
    }

    // 函数
    if (token.type === 'paren' && token.value === '(') {
      // 跳过左括号,获取下一个 token 作为函数名
      token = tokens[++current];

      let node = {
        type: 'CallExpression',
        name: token.value,
        params: []
      };

      // 再次自增 `current` 变量,获取参数 token
      token = tokens[++current];

      // 右括号之前的所有token都属于参数
      while ((token.type !== 'paren') || (token.type === 'paren' && token.value !== ')')) {
        node.params.push(walk());
        token = tokens[current];
      }

      // 跳过右括号
      current++;

      return node;
    }
    // 无法识别的字符,抛出错误提示
    throw new TypeError(token.type);
  }

  // AST的根节点
  let ast = {
    type: 'Program',
    body: [],
  };

  // 填充ast.body
  while (current < tokens.length) {
    ast.body.push(walk());
  }

  // 最后返回ast
  return ast;
}

通过递归来将词法分析器生成的词法单元转化为能够描述语法结构的 ast。

遍历

// 遍历器
function traverser(ast, visitor) {
  // 遍历 AST节点数组 对数组中的每一个元素调用 `traverseNode` 函数。
  function traverseArray(array, parent) {
    array.forEach(child => {
      traverseNode(child, parent);
    });
  }

  // 接受一个 `node` 和它的父节点 `parent` 作为参数
  function traverseNode(node, parent) {
    // 从 visitor 获取对应方法的对象
    let methods = visitor[node.type];
    // 通过 visitor 对应方法操作当前 node
    if (methods && methods.enter) {
      methods.enter(node, parent);
    }

    switch (node.type) {
      // 根节点
      case 'Program':
        traverseArray(node.body, node);
        break;
      // 函数调用
      case 'CallExpression':
        traverseArray(node.params, node);
        break;
      // 数值和字符串,不用处理
      case 'NumberLiteral':
      case 'StringLiteral':
        break;

      // 无法识别的字符,抛出错误提示
      default:
        throw new TypeError(node.type);
    }
    if (methods && methods.exit) {
      methods.exit(node, parent);
    }
  }
  // 开始遍历
  traverseNode(ast, null);
}

通过递归遍历 AST,在遍历过程中通过 visitor 对应方法操作当前 node,这里和切面差不多。

转换

// 转化器,参数:AST
function transformer(ast) {
  // 创建 `newAST`,它与之前的 AST 类似,Program:新AST的根节点
  let newAst = {
    type: 'Program',
    body: [],
  };

  // 通过 _context 维护新旧 AST,注意 _context 是一个引用,从旧的 AST 到新的 AST。
  ast._context = newAst.body;

  // 通过遍历器遍历 参数:AST 和 visitor
  traverser(ast, {
    // 数值,直接原样插入新AST
    NumberLiteral: {
      enter(node, parent) {
        parent._context.push({
          type: 'NumberLiteral',
          value: node.value,
        });
      },
    },
    // 字符串,直接原样插入新AST
    StringLiteral: {
      enter(node, parent) {
        parent._context.push({
          type: 'StringLiteral',
          value: node.value,
        });
      },
    },
    // 函数调用
    CallExpression: {
      enter(node, parent) {
        // 创建不同的AST节点
        let expression = {
          type: 'CallExpression',
          callee: {
            type: 'Identifier',
            name: node.name,
          },
          arguments: [],
        };

        // 函数调用有子类,建立节点对应关系,供子节点使用
        node._context = expression.arguments;

        // 顶层函数调用算是语句,包装成特殊的AST节点
        if (parent.type !== 'CallExpression') {

          expression = {
            type: 'ExpressionStatement',
            expression: expression,
          };
        }
        parent._context.push(expression);
      },
    }
  });
  // 最后返回新 AST
  return newAst;
}

这里通过 _context 引用维护新旧 AST,简单方便,但会污染旧AST。

代码生成

// 代码生成器 参数:新 AST
function codeGenerator(node) {

  switch (node.type) {
    // 遍历 body 属性中的节点,且递归调用 codeGenerator,结果按行输出
    case 'Program':
      return node.body.map(codeGenerator)
        .join('\n');

    // 表达式,处理表达式内容,并用分号结尾
    case 'ExpressionStatement':
      return (
        codeGenerator(node.expression) +
        ';'
      );

    // 函数调用,添加左右括号,参数用逗号隔开
    case 'CallExpression':
      return (
        codeGenerator(node.callee) +
        '(' +
        node.arguments.map(codeGenerator)
          .join(', ') +
        ')'
      );

    // 标识符,数值,原样输出
    case 'Identifier':
      return node.name;
    case 'NumberLiteral':
      return node.value;

    // 字符串,用双引号包起来再输出
    case 'StringLiteral':
      return '"' + node.value + '"';

    // 无法识别的字符,抛出错误提示
    default:
      throw new TypeError(node.type);
  }
}

根据转换后的新AST生成目标代码字符串。

编译器

function compiler(input) {
  let tokens = tokenizer(input);
  let ast    = parser(tokens);
  let newAst = transformer(ast);
  let output = codeGenerator(newAst);

  return output;
}

编译器整个工作流程:
1、input => tokenizer => tokens
2、tokens => parser => ast
3、ast => transformer => newAst
4、newAst => generator => output
将上面流程串起来,就构成了简单的编译器。

查看原文

赞 33 收藏 24 评论 0

hasaki 发布了文章 · 2018-09-11

redux 源码全方位剖析

版本:v4.0.0

后续内容更新,请前往:个人博客,欢迎一起交流。

前言

受2014年Facebook的Flux架构模式以及函数式编程语言Elm启发,Dan Abramov在2015年创建了 Redux。很快,Redux因其体小精悍(只有2kB)且没有任何依赖短时间内成为最热门的前端架构。

Redux 是可预测的状态管理框架,它很好的解决多交互,多数据源的诉求。Redux 设计之初,作者就严格遵循三个设计理念原则:
单一数据源:整个应用的 state 都被储存在一棵 object tree 中,并且这个 object tree 只存在于唯一一个 store 中。store 可以看做是数据存储的一个容器,在这个容器里面,只会维护唯一一个 state tree。store 会给定4种基础操作API:dispatch(action),getState(),replaceReducer(nextReducer),subscribe(listener)。根据单一数据源原则,所有数据会通过store.getState()方法调用获取。
state只读:根据 state 只读原则,数据变更会通过 store,dispatch(action) 方法,Action 可以理解为变更数据的信息载体,type 是变更数据的唯一标志,payload 是用来携带需要变更的数据,格式大致为:const action = { type: 'xxx', payload: 'yyy' };Reducer 是个纯函数,负责根据 action.type 获取需要变更的数据,然后计算 state 数值。格式为:reducer: prevState => action => newState
使用纯函数变更state值:Reducer 只是一些纯函数,它接收先前的 state 和 action,并返回新的 state。

正常的一个同步数据流为:view 层触发 actionCreator,actionCreator 通过 store.dispatch(action) 方法变更 reducer。但是面对多种多样的业务场景,同步数据流方式显然无法满足。对于改变reducer的异步数据操作,就需要用到中间件的概念,如图所示:

clipboard.png

在开始之前我们先做以下几点约定:
第一:目前分析的版本是 redux 的最新版本 4.0.0;
第二:我尽可能站在我自己的角度去剖析,当然我会借鉴社区比较优秀的文章,欢迎与大家一起交换意见,努力写好该 redux 源码系列;
第三:如果有幸您读到该 redux 源码系列,感觉写得还行,还望收藏、分享或打赏。

源码结构

Redux 的源码结构很简单,源码都在 src 目录下,其目录结构如下:

src
├── utils ---------------------------------------- 工具函数
├── applyMiddleware.js --------------------------- 加载 middleware
├── bindActionCreators.js ------------------------ 生成将 action creator 包裹在 dispatch 里的函数
├── combineReducers.js --------------------------- 合并 reducer 函数
├── compose.js ----------------------------------- 组合函数
├── createStore.js ------------------------------- 创建一个 Redux store 来储存应用中所有的 state
├── index.js ------------------------------------- 入口 js

源码入口

index.js 是整个代码的入口,其代码如下:

import createStore from './createStore'
import combineReducers from './combineReducers'
import bindActionCreators from './bindActionCreators'
import applyMiddleware from './applyMiddleware'
import compose from './compose'
import warning from './utils/warning'
import __DO_NOT_USE__ActionTypes from './utils/actionTypes'

function isCrushed() {}

if (
    process.env.NODE_ENV !== 'production' &&
    typeof isCrushed.name === 'string' &&
    isCrushed.name !== 'isCrushed'
) {
  warning(
    'You are currently using minified code outside of NODE_ENV === "production". ' +
      'This means that you are running a slower development build of Redux. ' +
      'You can use loose-envify (https://github.com/zertosh/loose-envify) for browserify ' +
      'or setting mode to production in webpack (https://webpack.js.org/concepts/mode/) ' +
      'to ensure you have the correct code for your production build.'
  )
}

export {
  createStore,
  combineReducers,
  bindActionCreators,
  applyMiddleware,
  compose,
  __DO_NOT_USE__ActionTypes
}

入口代码很简单,首先isCrushed函数主要是为了验证在非生产环境下Redux是否被压缩?如果被压缩了,isCrushed.name !== 'isCrushed' 就等于 true,这样就会给开发者一个warn提示。最后暴露createStorecombineReducersbindActionCreatorsapplyMiddlewarecompose 这几个接口给开发者使用,接下来我们逐一解析这几个 API。

createStore.js

createStore.js 是 Redux 最重要的一个 API ,它负责创建一个 Redux store 来储存应用中所有的 state,整个应用中应有且仅有一个 store。现在我们来看一下 createStore 源代码:

import $$observable from 'symbol-observable'

// 私有 action
import ActionTypes from './utils/actionTypes'
import isPlainObject from './utils/isPlainObject'

export default function createStore(reducer, preloadedState, enhancer) {

    // 判断接受的参数个数,来指定 reducer、preloadedState 和 enhancer
    if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
        enhancer = preloadedState
        preloadedState = undefined
    }

    // 如果 enhancer 存在且是个合法的函数,就调用 enhancer,否则抛出错误提示
    if (typeof enhancer !== 'undefined') {
        if (typeof enhancer !== 'function') {
            throw new Error('Expected the enhancer to be a function.')
        }

        return enhancer(createStore)(reducer, preloadedState)
    }

    if (typeof reducer !== 'function') {
        throw new Error('Expected the reducer to be a function.')
    }
    // 储存当前的 currentReducer
    let currentReducer = reducer
    // 储存当前的状态
    let currentState = preloadedState
    // 储存当前的监听函数列表
    let currentListeners = []
    // 储存下一个监听函数列表
    let nextListeners = currentListeners
    let isDispatching = false

    // 这个函数可以根据当前监听函数的列表生成新的下一个监听函数列表引用
    function ensureCanMutateNextListeners() {
        if (nextListeners === currentListeners) {
            nextListeners = currentListeners.slice()
        }
    }

  // 读取由 store 管理的状态树
  function getState() {
    if (isDispatching) {
      throw new Error(
        'You may not call store.getState() while the reducer is executing. ' +
          'The reducer has already received the state as an argument. ' +
          'Pass it down from the top reducer instead of reading it from the store.'
      )
    }

    return currentState
  }

  function subscribe(listener) {
    // 判断传入的参数是否为函数
    if (typeof listener !== 'function') {
      throw new Error('Expected the listener to be a function.')
    }

    if (isDispatching) {
      throw new Error(
        'You may not call store.subscribe() while the reducer is executing. ' +
          'If you would like to be notified after the store has been updated, subscribe from a ' +
          'component and invoke store.getState() in the callback to access the latest state. ' +
          'See https://redux.js.org/api-reference/store#subscribe(listener) for more details.'
      )
    }

    let isSubscribed = true

    ensureCanMutateNextListeners()
    nextListeners.push(listener)

    return function unsubscribe() {
      if (!isSubscribed) {
        return
      }

      if (isDispatching) {
        throw new Error(
          'You may not unsubscribe from a store listener while the reducer is executing. ' +
            'See https://redux.js.org/api-reference/store#subscribe(listener) for more details.'
        )
      }

      isSubscribed = false

      ensureCanMutateNextListeners()
      const index = nextListeners.indexOf(listener)
      nextListeners.splice(index, 1)
    }
  }

  function dispatch(action) {
    if (!isPlainObject(action)) {
      throw new Error(
        'Actions must be plain objects. ' +
          'Use custom middleware for async actions.'
      )
    }
    // 判断 action 是否有 type{必须} 属性
    if (typeof action.type === 'undefined') {
      throw new Error(
        'Actions may not have an undefined "type" property. ' +
          'Have you misspelled a constant?'
      )
    }
    // 如果正在 dispatch 则抛出错误
    if (isDispatching) {
      throw new Error('Reducers may not dispatch actions.')
    }
    // 对抛出 error 的兼容,但是无论如何都会继续执行 isDispatching = false 的操作
    try {
      isDispatching = true
      // 使用 currentReducer 来操作传入 当前状态和 action,放回处理后的状态
      currentState = currentReducer(currentState, action)
    } finally {
      isDispatching = false
    }

    const listeners = (currentListeners = nextListeners)
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i]
      listener()
    }

    return action
  }

  // 判断参数是否是函数类型
  function replaceReducer(nextReducer) {
    if (typeof nextReducer !== 'function') {
      throw new Error('Expected the nextReducer to be a function.')
    }

    currentReducer = nextReducer
    dispatch({ type: ActionTypes.REPLACE })
  }

  function observable() {
    const outerSubscribe = subscribe
    return {
      subscribe(observer) {
        if (typeof observer !== 'object' || observer === null) {
          throw new TypeError('Expected the observer to be an object.')
        }

        function observeState() {
          if (observer.next) {
            observer.next(getState())
          }
        }

        observeState()
        const unsubscribe = outerSubscribe(observeState)
        return { unsubscribe }
      },

      [$$observable]() {
        return this
      }
    }
  }

  dispatch({ type: ActionTypes.INIT })

  return {
    dispatch,
    subscribe,
    getState,
    replaceReducer,
    [$$observable]: observable
  }
}

这里我们首先要讲一下ActionTypes对象,它是 Redux 的私有 action,不允许外界触发,用来初始化 store 的状态树和改变 reducers 后初始化 store 的状态树。接下来我们从不同角度着重来讲一下 createStore 函数:

参数

它可以接受三个参数:reducer、preloadedState、enhancer:
reducer:函数,返回下一个状态,接受两个参数:当前状态 和 触发的 action;
preloadedState:它是 state 的初始值,可以随意指定,比如服务端渲染的初始状态,但是如果使用 combineReducers 来生成 reducer,那必须保持状态对象的 key 和 combineReducers 中的 key 相对应,另外实际上它并不仅仅是扮演着一个 initialState 的角色,如果我们第二个参数是函数类型,createStore 会认为我们忽略了 preloadedState 而传入了一个enhancer;
enhancer:可选参数,一个组合 store creator 的高阶函数,可以翻译成 store 的增强器,顾名思义,就是增强 store 的功能。一般指定为第三方的中间件,时间旅行,持久化等等,返回一个新的强化过的 store creator,这个函数通常用 Redux 提供的 applyMiddleware 函数来生成。

根据传入参数的个数和类型,判断 reducer、preloadedState、enhancer。

返回值

调用完函数的返回值:dispatch、subscribe、getState、replaceReducer 和 [$$observable],这就是我们开发中主要使用的几个接口。

enhancer

如果enhancer参数存在且是个合法的函数,那么就调用enhancer函数。enhancer实际上是一个高阶函数,它的参数是创建store的函数createStore,返回值是一个可以创建功能更加强大的store的函数(enhanced store creator),这和 React 中的高阶组件的概念很相似。store enhancer 函数的结构一般如下:

function enhancerCreator() {
  return createStore => (...args) => {
    // do something based old store
    // return a new enhanced store
  }
}

注意,enhancerCreator是用于创建enhancer store的函数,也就是说enhancerCreator的执行结果才是一个enhancer store...args参数代表创建store所需的参数,也就是createStore接收的参数,实际上就是(reducer, [preloadedState], [enhancer])

现在,我们来创建一个enhancer store,用于输出发送的action的信息和state的变化:

// logging.js(store enhancer)
export default function logging() {
  return createStore => (reducer, initialState, enhancer) => {
    const store = createStore(reducer, initialState, enhancer)
    function dispatch(action) {
      console.log(`dispatch an action: ${JSON.stringify(action)}`);
      const res = store.dispatch(action);
      const newState = store.getState();
      console.log(`current state: ${JSON.stringify(newState)}`);
      return res;
    }
    return {...store, dispatch}
  }
}

logging()改变了store dispatch的默认行为,在每次发送action前后,都会输出日志信息,然后在创建store上,使用logging()这个store enhancer:

// store.js
import { createStore, combineReducers } from 'redux';
import * as reducer from '../reducer';
import logging from '../logging';

//创建一个 Redux store 来以存放应用中所有的 state,应用中应有且仅有一个 store。

var store = createStore(
    combineReducers(reducer),
    logging()
);

export default store;

getState

// 读取由 store 管理的状态树
function getState() {
  if (isDispatching) {
    throw new Error(
      'You may not call store.getState() while the reducer is executing. ' +
        'The reducer has already received the state as an argument. ' +
        'Pass it down from the top reducer instead of reading it from the store.'
    )
  }

  return currentState
}

这个函数可以获取当前的状态,createStore 中的 currentState 储存当前的状态树,这是一个闭包,这个参数会持久存在,并且所有的操作状态都是改变这个引用,getState 函数返回当前的 currentState。

subscribe

function subscribe(listener) {
  // 判断传入的参数是否为函数
  if (typeof listener !== 'function') {
    throw new Error('Expected the listener to be a function.')
  }

  if (isDispatching) {
    throw new Error(
      'You may not call store.subscribe() while the reducer is executing. ' +
        'If you would like to be notified after the store has been updated, subscribe from a ' +
        'component and invoke store.getState() in the callback to access the latest state. ' +
        'See https://redux.js.org/api-reference/store#subscribe(listener) for more details.'
    )
  }

  let isSubscribed = true

  ensureCanMutateNextListeners()
  nextListeners.push(listener)

  return function unsubscribe() {
    if (!isSubscribed) {
      return
    }

    if (isDispatching) {
      throw new Error(
        'You may not unsubscribe from a store listener while the reducer is executing. ' +
          'See https://redux.js.org/api-reference/store#subscribe(listener) for more details.'
      )
    }

    isSubscribed = false

    ensureCanMutateNextListeners()
    const index = nextListeners.indexOf(listener)
    nextListeners.splice(index, 1)
  }
}

这个函数可以给 store 的状态添加订阅监听函数,一旦调用dispatch,所有的监听函数就会执行;nextListeners就是储存当前监听函数的列表,调用subscribe,传入一个函数作为参数,那么就会给nextListeners列表push这个函数;同时调用subscribe函数会返回一个unsubscribe函数,用来解绑当前传入的函数,同时在subscribe函数定义了一个isSubscribed标志变量来判断当前的订阅是否已经被解绑,解绑的操作就是从nextListeners列表中删除当前的监听函数。

dispatch

function dispatch(action) {
    if (!isPlainObject(action)) {
      throw new Error(
        'Actions must be plain objects. ' +
          'Use custom middleware for async actions.'
      )
    }
    // 判断 action 是否有 type{必须} 属性
    if (typeof action.type === 'undefined') {
      throw new Error(
        'Actions may not have an undefined "type" property. ' +
          'Have you misspelled a constant?'
      )
    }
    // 如果正在 dispatch 则抛出错误
    if (isDispatching) {
      throw new Error('Reducers may not dispatch actions.')
    }
    // 对抛出 error 的兼容,但是无论如何都会继续执行 isDispatching = false 的操作
    try {
      isDispatching = true
      // 使用 currentReducer 来操作传入 当前状态和 action,放回处理后的状态
      currentState = currentReducer(currentState, action)
    } finally {
      isDispatching = false
    }

    const listeners = (currentListeners = nextListeners)
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i]
      listener()
    }

    return action
  }

这个函数是用来触发状态改变的,它接受一个 action 对象作为参数,然后 reducer 根据 action 的属性以及当前 store 的状态来生成一个新的状态,赋予当前状态,改变 store 的状态;即currentState = currentReducer(currentState, action);这里的currentReducer是一个函数,它接受两个参数:当前状态 和 action,然后返回计算出来的新的状态;然后遍历nextListeners列表,调用每个监听函数。

replaceReducer

// 判断参数是否是函数类型
function replaceReducer(nextReducer) {
  if (typeof nextReducer !== 'function') {
    throw new Error('Expected the nextReducer to be a function.')
  }

  currentReducer = nextReducer
  dispatch({ type: ActionTypes.REPLACE })
}

这个函数可以替换 store 当前的 reducer 函数,首先直接用currentReducer = nextReducer替换;然后dispatch({ type: ActionTypes.INIT }),用来初始化替换后 reducer 生成的初始化状态并且赋予 store 的状态。

observable

function observable() {
  const outerSubscribe = subscribe
  return {
    subscribe(observer) {
      if (typeof observer !== 'object' || observer === null) {
        throw new TypeError('Expected the observer to be an object.')
      }

      function observeState() {
        if (observer.next) {
          observer.next(getState())
        }
      }

      observeState()
      const unsubscribe = outerSubscribe(observeState)
      return { unsubscribe }
    },

    [$$observable]() {
      return this
    }
  }
}

对于这个函数,是不直接暴露给开发者的,它提供了给其他观察者模式/响应式库的交互操作。

初始化 store 的状态

最后执行dispatch({ type: ActionTypes.INIT }),用来根据 reducer 初始化 store 的状态。

compose.js

compose可以接受一组函数参数,然后从右到左来组合多个函数(这是函数式编程中的方法),最后返回一个组合函数。现在我们来看一下 compose 源代码:

/**
 * For example, compose(f, g, h) is identical to doing
 * (...args) => f(g(h(...args))).
 */
export default function compose(...funcs) {
  if (funcs.length === 0) {
    return arg => arg
  }

  if (funcs.length === 1) {
    return funcs[0]
  }

  return funcs.reduce((a, b) => (...args) => a(b(...args)))
}

compose其作用是把一系列的函数,组装生成一个新的函数,并且从后到前,后面参数的执行结果作为其前一个的参数,当需要把多个 store 增强器 依次执行的时候,需要用到它。

参数

(...funcs):需要合成的多个函数。每个函数都接收一个函数作为参数,然后返回一个函数。

返回值

(Function):从右到左把接收到的函数合成后的最终函数。

applyMiddleware.js

It provides a third-party extension point between dispatching an action, and the moment it reaches the reducer.

这是 redux 作者 Dan 对 middleware 的描述,middleware 提供了一个分类处理 action 的机会,在 middleware 中我们可以检阅每一个流过的 action,挑选出特定类型的 action 进行相应操作,给我们改变 action 的机会。
Redux middleware 的设计十分特殊,是一个层层包裹的匿名函数,实际上这是函数式编程中的柯里化,一种使用匿名单参数函数来实现多参数函数的方法,柯里化的 middleware 结构好处在于:
一:易串联,柯里化函数具有延迟执行的特性,通过不断柯里化形成的 middleware 可以累积参数,配合组合的方式,很容易形成 pipeline 来处理数据流。
二:共享 store,在 applyMiddleware 执行过程中,store 还是旧的,但是因为闭包的存在,applyMiddleware 完成后,所有的 middlewares 内部拿到的 store 是最新且相同的。

redux 提供了 applyMiddleware 这个 api 来加载 middleware。现在我们来看一下 applyMiddleware 源代码:

import compose from './compose'

export default function applyMiddleware(...middlewares) {
  return createStore => (...args) => {
    const store = createStore(...args)
    let dispatch = () => {
      throw new Error(
        `Dispatching while constructing your middleware is not allowed. ` +
          `Other middleware would not be applied to this dispatch.`
      )
    }
    // 暴漏 getState 和 dispatch 供第三方中间件使用
    const middlewareAPI = {
      getState: store.getState,
      dispatch: (...args) => dispatch(...args)
    }
    // middlewareAPI 作为每个 middleware 的参数依次执行一遍,最终返回函数组成的数组
    const chain = middlewares.map(middleware => middleware(middlewareAPI))
    // 用 compose 组合函数生成新的 dispatch
    dispatch = compose(...chain)(store.dispatch)

    return {
      ...store,
      dispatch
    }
  }
}

由上我们可以发现 applyMiddleware 的结构也是一个多层柯里化的函数,借助 compose,applyMiddleware 可以用来和其他插件一起加强 createStore 函数。

参数

我们在 createStore 小节中其实就用提及过 applyMiddleware:

// 如果 enhancer 存在且是个合法的函数,就调用 enhancer,否则抛出错误提示
if (typeof enhancer !== 'undefined') {
  if (typeof enhancer !== 'function') {
    throw new Error('Expected the enhancer to be a function.')
  }

  return enhancer(createStore)(reducer, preloadedState)
}

这里 enhancer 其实就等于 applyMiddleware(mid1, mid2, mid3, ...),因此我们创建一个 store 实际上就变成如下方式了:

applyMiddleware(mid1, mid2, mid3, ...)(createStore)(reducer, preloadedState);

由上述代码可知 applyMiddleware 陆续可以接受四个参数:
[md1, mid2, mid3, ...]: middlewares 数组;
createStore:Redux 原生的 createStore;
reducer:函数,返回下一个状态;
preloadedState:state 的初始值。
接下来,我们看一下 applyMiddleware 用这些参数都做了什么?

const store = createStore(...args)
const middlewareAPI = {
  getState: store.getState,
  dispatch: (...args) => dispatch(...args)
}

const chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)

applyMiddleware 利用 createStore 和 (reducer, preloadedState) 创建了一个 store,然后 store 的 getState 方法和 dispatch 方法又分别赋值给 middlewareAPI 变量,紧接着用 middlewareAPI 作为每个 middleware 的参数依次执行一遍,执行完后,最终获得数组 chain:[f1, f2, ..., fn] 交给组合函数 compose 处理。compose 可以接受一组函数参数,然后从右到左来组合多个函数(这是函数式编程中的方法),最后返回一个组合函数,例如:

// 调用
dispatch = compose(f, g, h)(store.dispatch)
// 返回
dispatch = f(g(h(store.dispatch)))

这样通过调用新的 dispatch,每个 middleware 的代码就可以依次执行了。

返回值

store:原来的store;
dispatch:改变后的dispatch。

combineReducers.js

Reducer 是管理 state 的一个模块,它主要做的事情就是当项目初始化时,返回 initalState,当用户用操作时,它会根据 action 进行相应地更新。需要注意的是它是一个纯函数,换言之,它不会改变传入的 state。现在我们来看一下 combineReducers 源码(源码有删减,删除了一些验证代码):

import ActionTypes from './utils/actionTypes'
import warning from './utils/warning'
import isPlainObject from './utils/isPlainObject'

export default function combineReducers(reducers) {
  // 根据 reducers 生成最终合法的 finalReducers:value 为 函数
  const reducerKeys = Object.keys(reducers)
  const finalReducers = {}
  for (let i = 0; i < reducerKeys.length; i++) {
    const key = reducerKeys[i]

    if (process.env.NODE_ENV !== 'production') {
      if (typeof reducers[key] === 'undefined') {
        warning(`No reducer provided for key "${key}"`)
      }
    }

    if (typeof reducers[key] === 'function') {
      finalReducers[key] = reducers[key]
    }
  }
  const finalReducerKeys = Object.keys(finalReducers)

  let unexpectedKeyCache
  if (process.env.NODE_ENV !== 'production') {
    unexpectedKeyCache = {}
  }

  let shapeAssertionError
  try {
    assertReducerShape(finalReducers)
  } catch (e) {
    shapeAssertionError = e
  }

  // 返回最终生成的 reducer
  return function combination(state = {}, action) {
    if (shapeAssertionError) {
      throw shapeAssertionError
    }

    if (process.env.NODE_ENV !== 'production') {
      const warningMessage = getUnexpectedStateShapeWarningMessage(
        state,
        finalReducers,
        action,
        unexpectedKeyCache
      )
      if (warningMessage) {
        warning(warningMessage)
      }
    }

    let hasChanged = false
    const nextState = {}
    for (let i = 0; i < finalReducerKeys.length; i++) {
      const key = finalReducerKeys[i]
      const reducer = finalReducers[key]
      const previousStateForKey = state[key]
      const nextStateForKey = reducer(previousStateForKey, action)
      if (typeof nextStateForKey === 'undefined') {
        const errorMessage = getUndefinedStateErrorMessage(key, action)
        throw new Error(errorMessage)
      }
      nextState[key] = nextStateForKey
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey
    }
    // 遍历一遍验证下是否改变,然后返回原有状态值或者新的状态值
    return hasChanged ? nextState : state
  }
}

该函数最终返回 combination 函数,它就是真正 createStore 函数的 reducer,接受一个初始化状态和一个 action 参数;该函数每次调用大致执行以下几个操作:
1、for (let i = 0; i < finalReducerKeys.length; i++) { ... }:遍历 finalReducer(有效的 reducer 列表);
2、var previousStateForKey = state[key]:当前遍历项的之前状态,看到这里就应该明白传入的 reducers 组合为什么 key 要和 store 里面的 state 的 key 相对应了;
3、var nextStateForKey = reducer(previousStateForKey, action):当前遍历项的下一个状态;
4、nextState[key] = nextStateForKey:将 当前遍历项的下一个状态添加到 nextState;
5、hasChanged = hasChanged || nextStateForKey !== previousStateForKey:判断状态是否改变;
6、return hasChanged ? nextState : state:如果没有改变就返回原有状态,如果改变了就返回新生成的状态对象。

参数

reducers (Object): 一个对象,它的值(value)对应不同的 reducer 函数,这些 reducer 函数后面会被合并成一个。

返回值

(Function):它是真正 createStore 函数的 reducer,接受一个初始化状态和一个 action 参数;每次调用的时候会去遍历 finalReducer(有效的 reducer 列表),然后调用列表中每个 reducer,最终构造一个与 reducers 对象结构相同的 state 对象。

bindActionCreators.js

Redux 中的 bindActionCreators 是通过 dispatch 将 action 包裹起来,这样就可以通过 bindActionCreators 创建方法调用 dispatch(action)。现在我们来看一下 bindActionCreators 源代码:

function bindActionCreator(actionCreator, dispatch) {
  return function() {
    return dispatch(actionCreator.apply(this, arguments))
  }
}

export default function bindActionCreators(actionCreators, dispatch) {
  // 如果是一个函数,直接返回一个 bindActionCreator 函数,即调用 dispatch 触发 action
  if (typeof actionCreators === 'function') {
    return bindActionCreator(actionCreators, dispatch)
  }

  if (typeof actionCreators !== 'object' || actionCreators === null) {
    throw new Error(
      `bindActionCreators expected an object or a function, instead received ${
        actionCreators === null ? 'null' : typeof actionCreators
      }. ` +
        `Did you write "import ActionCreators from" instead of "import * as ActionCreators from"?`
    )
  }
  // 遍历对象,然后设置每个遍历项的 actionCreator 生成函数,最后返回这个对象
  const keys = Object.keys(actionCreators)
  const boundActionCreators = {}
  for (let i = 0; i < keys.length; i++) {
    const key = keys[i]
    const actionCreator = actionCreators[key]
    if (typeof actionCreator === 'function') {
      boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
    }
  }
  return boundActionCreators
}

由此可以看出 bindActionCreators 的实现逻辑比较简单:
一、判断传入的参数是否是 object,如果是函数,就直接返回一个将 action creator 包裹在 dispatch 里的函数。
二、如果是object,就根据相应的key,生成相应的将 action creator 包裹在 dispatch 里的函数。

为了方便理解,我们用一个 TODO 例子说明下:

// actions/todo.js
export function addTodo(text) {
  return {
    type: 'ADD_TODO',
    text
  }
}

export function removeTodo(id) {
  return {
    type: 'REMOVE_TODO',
    id
  }
}

我们 import 后会得到如下对象:

{
   addTodo : text => 
    { 
      type: 'ADD_TODO',
      text
    },
   removeTodo : id => {
      type: 'REMOVE_TODO',
      id
    }
}

经过 bindActionCreator 就会变成 key 相同,值为用 dispatch 将 action creator 包裹起来的函数的对象:

{
   addTodo : text => dispatch(addTodo('text'));
   removeTodo : id => dispatch(removeTodo('id'));
}

由此我们发现可以通过 bindActionCreators 创建方法直接调用 dispatch(action)。

参数

它可以接收两个参数:actionCreators、dispatch
actionCretors:可以是一个对象,也可以是一个单个函数
dispatch:dispatch 函数,从 store 实例中获取,用于包裹 action creator

如果只是传入一个 function,返回的也是一个 function,例如:

// actions/todo.js
export const toggleTodo = (id) => {
  return {
      type: 'TOGGLE_TODO',
      id
  };
};

经过 bindActionCreator:

const boundActionCreators = bindActionCreators(toggleTodo, dispatch);

由于 bindActionCreators 第一个参数是一个函数,结果就会变为:

const boundActionCreators  = (id) => dispatch(toggleTodo(id));

返回值

单个函数,或者是一个对象。

总结

通过阅读 Redux 的源码,我们发现 Redux 设计的实在是太精妙了,完全函数式编程,依赖少,耦合低。

查看原文

赞 18 收藏 17 评论 0

hasaki 回答了问题 · 2018-09-06

解决我想把数组转换为对象,该怎么转换呢

Object.assign(...arr)

简要分析:

对象合并(Object.assign)加 扩展运算符(...

关注 5 回答 2

hasaki 评论了文章 · 2018-08-24

next.js、nuxt.js等服务端渲染框架构建的项目部署到服务器,并用PM2守护程序

前端渲染:vue、react等单页面项目应该这样子部署到服务器

貌似从前几年,前后端分离逐渐就开始流行起来,把一些渲染计算的工作抛向前端以便减轻服务端的压力,但为啥现在又开始流行在服务端渲染了呢?如vue全家桶或者react全家桶,都推荐通过服务端渲染来实现路由。搞得我们慌得不行,不禁让我想起一句话:从来没有任何一门语言的技术栈像Javascript一样,学习者拼尽全力也不让精通。没办法,流行,咱们就得学!

前断时间写了一篇vue、react等单页面项目应该这样子部署到服务器,结果反响不错!最近好多朋友私信或邀请问很多关于next.js和nuxt.js的问题,比如关于nextjs 和 nuxtjs如何部署?pm2如何配合?...在这里我们就一起讨论下在服务器上使用PM2守护next.js、nuxt.js等服务端渲染框架构建的项目!该篇我们只讨论服务端渲染应用部署静态应用部署就是我前段时间写的vue、react等单页面项目应该这样子部署到服务器

Nginx配置

既然是应用,我们就应该有域名,在这里我们以 nginx配置 为例,简单配置如下:
Next域名:http://next.sosout.com/
Nuxt域名:http://nuxt.sosout.com/

http {
    ....  # 省略其他配置
   
    server {
        listen 80;
        server_name  *.sosout.com;
        
        if ($host ~* "^(.*?)\.sosout\.com$") {
            set $domain $1;
        }

        location / {
            if ($domain ~* "next") {
                root /mnt/html/next;
            }
            if ($domain ~* "nuxt") {
                root /mnt/html/nuxt;
            }
            proxy_set_header   Host             $host;
            proxy_set_header   X-Real-IP        $remote_addr;
            proxy_set_header   X-Forwarded-For  $proxy_add_x_forwarded_for;
            proxy_set_header   X-Forwarded-Proto  $scheme;
        }
        access_log  /mnt/logs/nginx/access.log  main;
    }

    #tcp_nopush     on;

    include /etc/nginx/conf.d/*.conf;
}

Nginx反向代理

由于服务端渲染的各个应用端口号各不相同,因此这个时候我们就需要反向代理了,配置如下:

#通过upstream nodejs 可以配置多台nodejs节点,做负载均衡
#keepalive 设置存活时间。如果不设置可能会产生大量的timewait
#proxy_pass 反向代理转发 http://nodejs

upstream nodenext {
    server 127.0.0.1:3001; #next项目 监听端口
    keepalive 64;
}

server {
    listen 80;
    server_name next.sosout.com;
    location / {
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;  
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Nginx-Proxy true;
        proxy_cache_bypass $http_upgrade;
        proxy_pass http://nodenext; #反向代理
    }
}

upstream nodenuxt {
    server 127.0.0.1:3002; #nuxt项目 监听端口
    keepalive 64;
}

server {
    listen 80;
    server_name nuxt.sosout.com;
    location / {
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;  
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Nginx-Proxy true;
        proxy_cache_bypass $http_upgrade;
        proxy_pass http://nodenuxt; #反向代理
    }
}

服务器的准备工作已完成,接下来我们就分别看看Next.js和Nuxt.js服务端渲染应用如何部署?

Next.js服务端渲染应用部署

部署 Next.js 服务端渲染的应用不能直接使用 next 命令,而应该先进行编译构建,然后再启动 Next 服务,官方通过以下两个命令来完成:

next build
next start

官方推荐的 package.json 配置如下:

{
  "name": "my-app",
  "dependencies": {
    "next": "latest"
  },
  "scripts": {
    "dev": "next",
    "build": "next build",
    "start": "next start"
  }
}

而我更推荐如下配置,稍后你会发现这样和 pm2 一起使用更方便,自动化部署也方便:

{
  "name": "my-app",
  "dependencies": {
    "next": "latest"
  },
  "scripts": {
    "dev": "next",
    "start": "next start -p $PORT",
    "build": "next build && PORT=3001 npm start"
  }
}

next.js服务端渲染应用部署这样就完成了,官方先后执行 npm run build 、npm start 即可完成部署。而我这边只要执行 npm run build ,其实我只是把两个合并成一个,并设置了端口以便区别其他应用,避免端口占用!

接下来简单的说一下next这几个命令:
next: 启动一个热加载的Web服务器(开发模式)
next build: 利用webpack编译应用,压缩JS和CSS资源(发布用)。
next start: 以生成模式启动一个Web服务器 (next build 会先被执行)。

Nuxt.js服务端渲染应用部署

其实部署 Nuxt.js 服务端渲染的应用和 Next.js 极其相似!在这里我就把代码粘粘贴贴,复复制制,改改写写。。。。
Nuxt.js 服务端渲染的应用不能直接使用 nuxt 命令,而应该先进行编译构建,然后再启动 Nuxt 服务,官方通过以下两个命令来完成:

nuxt build
nuxt start

官方推荐的 package.json 配置如下:

{
  "name": "my-app",
  "dependencies": {
    "nuxt": "latest"
  },
  "scripts": {
    "dev": "nuxt",
    "build": "nuxt build",
    "start": "nuxt start"
  }
}

而我更推荐如下配置,稍后你会发现这样和 pm2 一起使用更方便,自动化部署也方便:

{
  "name": "my-app",
  "dependencies": {
    "nuxt": "latest"
  },
  "scripts": {
    "dev": "nuxt",
    "start": "PORT=3002 nuxt start",
    "build": "nuxt build && npm start"
  }
}

nuxt.js服务端渲染应用部署这样就完成了,官方先后执行 npm run build 、npm start 即可完成部署。而我这边只要执行 npm run build ,其实我只是把两个合并成一个,并设置了端口以便区别其他应用,避免端口占用!

接下来简单的说一下nuxt这几个命令:
nuxt: 启动一个热加载的Web服务器(开发模式)
nuxt build: 利用webpack编译应用,压缩JS和CSS资源(发布用)。
nuxt start: 以生成模式启动一个Web服务器 (nuxt build 会先被执行)。

PM2守护程序

Next.js使用pm2,进入对应的应用目录,执行以下命令:

pm2 start npm --name "my-next" -- run build

Nuxt.js使用pm2,进入对应的应用目录,执行以下命令:

pm2 start npm --name "my-nuxt" -- run build

使用pm2时,把两个部署命令合成一个更方便!执行完pm2的启动命令后,我们用 pm2 list 查看一下进程列表,我截一下我个人服务器的pm2列表:

clipboard.png

以后您就可以用pm2进行维护了,比如我们的next应用更改了代码,因为当时创建时给next应用命名的进程名称为 my-next ,因此我们可以直接使用 pm2 reload my-next 进行重载。接下来我就简单介绍一下pm2,如果有需要,我可以另写一篇关于pm2的文章!

pm2 简单介绍

pm2是nodejs的一个带有负载均衡功能的应用进程管理器的模块,类似有Supervisor,forever,用来进行进程管理。

一、安装:
npm install pm2 -g
二、启动:
pm2 start app.js
pm2 start app.js --name my-api       #my-api为PM2进程名称
pm2 start app.js -i 0                #根据CPU核数启动进程个数
pm2 start app.js --watch             #实时监控app.js的方式启动,当app.js文件有变动时,pm2会自动reload
三、查看进程:
pm2 list
pm2 show 0 或者 # pm2 info 0         #查看进程详细信息,0为PM2进程id 
四、监控:
pm2 monit
五、停止:
pm2 stop all                         #停止PM2列表中所有的进程
pm2 stop 0                           #停止PM2列表中进程为0的进程
六、重载:
pm2 reload all                       #重载PM2列表中所有的进程
pm2 reload 0                         #重载PM2列表中进程为0的进程
七、重启:
pm2 restart all                      #重启PM2列表中所有的进程
pm2 restart 0                        #重启PM2列表中进程为0的进程
八、删除PM2进程:
pm2 delete 0                         #删除PM2列表中进程为0的进程
pm2 delete all                       #删除PM2列表中所有的进程
九、日志操作:
pm2 logs [--raw]                     #Display all processes logs in streaming
pm2 flush                            #Empty all log file
pm2 reloadLogs                       #Reload all logs
十、升级PM2:
npm install pm2@lastest -g           #安装最新的PM2版本
pm2 updatePM2                        #升级pm2
十一、更多命令参数请查看帮助:
pm2 --help
十二、PM2目录结构:
  • 1、默认的目录是:当前用于的家目录下的.pm2目录(此目录可以自定义,请参考:十三、自定义启动文件),详细信息如下:
$HOME/.pm2                   #will contain all PM2 related files
$HOME/.pm2/logs              #will contain all applications logs
$HOME/.pm2/pids              #will contain all applications pids
$HOME/.pm2/pm2.log           #PM2 logs
$HOME/.pm2/pm2.pid           #PM2 pid
$HOME/.pm2/rpc.sock          #Socket file for remote commands
$HOME/.pm2/pub.sock          #Socket file for publishable events
$HOME/.pm2/conf.js           #PM2 Configuration
十三、自定义启动文件:
  • 1、创建一个test.json的示例文件,格式如下:
{
  "apps":
    {
      "name": "test",
      "cwd": "/data/wwwroot/nodejs",
      "script": "./test.sh",
      "exec_interpreter": "bash",
      "min_uptime": "60s",
      "max_restarts": 30,
      "exec_mode" : "cluster_mode",
      "error_file" : "./test-err.log",
      "out_file": "./test-out.log",
      "pid_file": "./test.pid"
      "watch": false
    }
}
  • 2、参数说明:
apps:json结构,apps是一个数组,每一个数组成员就是对应一个pm2中运行的应用
name:应用程序的名称
cwd:应用程序所在的目录
script:应用程序的脚本路径
exec_interpreter:应用程序的脚本类型,这里使用的shell,默认是nodejs
min_uptime:最小运行时间,这里设置的是60s即如果应用程序在60s内退出,pm2会认为程序异常退出,此时触发重启max_restarts设置数量
max_restarts:设置应用程序异常退出重启的次数,默认15次(从0开始计数)
exec_mode:应用程序启动模式,这里设置的是cluster_mode(集群),默认是fork
error_file:自定义应用程序的错误日志文件
out_file:自定义应用程序日志文件
pid_file:自定义应用程序的pid文件
watch:是否启用监控模式,默认是false。如果设置成true,当应用程序变动时,pm2会自动重载。这里也可以设置你要监控的文件。

由于工作原因,一直没光顾segmentfault,收到很多关于部署的私信和评论,特此补充以下内容:

部署(以nuxt为例)

基础模板的部署方式

何为基础模板?使用了 vue init nuxt-community/starter-template <project-name> 进行搭建的!

第一步,打包

在执行 npm run build 的时候, nuxt 会自动打包。

第二步,选择要部署的文件(社友最关心的步骤):

  • .nuxt/ 文件夹
  • package.json 文件
  • nuxt.config.js 文件(如果你配置proxy等,则需要上传这个文件,建议把它传上去)

第三步,启动你的nuxt:

使用pm2启动你的nuxt.js:

$ npm install // or yarn install 如果未安装依赖或依赖有更改
$ pm2 start npm --name "my-nuxt" -- run start
查看原文

hasaki 发布了文章 · 2018-08-21

React 源码全方位剖析第一章- 前置知识

版本:v16.4.1

查看所有章节以及后续更新,请前往:个人博客,欢迎一起交流。

前言

当时在各种前端框架或库充斥市场的情况下,出现了大量优秀的框架,比如 Backbone、Angular、Knockout、Ember 这些框架大都采用了 MV* 的理念,把数据与视图分离。而就在这样纷繁复杂的时期,React 诞生于 Facebook 的内部项目,因为该公司对市场上所有 JavaScript MVC 框架,都不满意,就决定自己写一套,用来架设 Instagram 的网站。做出来以后,发现这套东西很好用,就在2013年5月开源了。所谓知其然还要知其所以然,加上 React 真是一天一改,如果现在不看,以后也真的很难看懂了。目前社区有很多 React 的源码剖析文章,趁着最近工作不忙,我打算分享一下 React 源码,并自形成一个系列,欢迎一起交流。在开始之前我们先做以下几点约定:

第一:目前分析的版本是 React 的最新版本 16.4.1。
第二:我尽可能站在我自己的角度去剖析,当然我会借鉴社区比较优秀的文章,同时面对大家的拍砖,我无条件接受,也很乐意与大家一起交换意见,努力写好该 React 源码系列。
第三:如果有幸您读到该 React 源码系列,感觉写得还行,还望收藏、分享或打赏。

前置知识

我们从这一章开始即将分析 React 的源码,在分析源码之前我们很有必要介绍一些前置知识如flow、Rollup等。除此之外,我们最好已经用过 React 做过实际项目,对 React 的思想有了一定的了解,对绝大部分的 API 都已经有使用,同时,我们应该有一定的HTML、CSS、JavaScript、ES6+、node & npm等功底,并对代码调试有一定的了解。

如果具备了以上条件,并且对 React 的实现原理很感兴趣,那么就可以开始 React 的底层学习了,对它的实现细节一探究竟。

Flow - JavaScript静态类型检查工具

Flow 是 facebook 出品的 JavaScript 静态类型检查工具,它与 Typescript 不同的是,它可以部分引入,不需要完全重构整个项目,所以对于一个已有一定规模的项目来说,迁移成本更小,也更加可行。除此之外,Flow 可以提供实时增量的反馈,通过运行 Flow server 不需要在每次更改项目的时候完全从头运行类型检查,提高运行效率。可以简单总结为:对于新项目,可以考虑使用 TypeScript 或者 Flow,对于已有一定规模的项目则建议使用 Flow 进行较小成本的逐步迁移来引入类型检查。React 的源码利用了 Flow 做了静态类型检查,所以了解 Flow 有助于我们阅读源码。

为什么用静态类型检查工具 Flow

JavaScript 是动态类型语言,它的灵活性有目共睹,但是过于灵活的副作用就是很容易就写出非常隐蔽的隐患代码,在编译期甚至运行时看上去都不会报错,但是可能会发生各种各样奇怪的和难以解决的bug。

类型检查是当前动态类型语言的发展趋势,所谓类型检查,就是在编译期尽早发现(由类型错误引起的)bug,又不影响代码运行(不需要运行时动态检查类型),使编写 JavaScript 具有和编写 Java 等强类型语言相近的体验。

项目越复杂就越需要通过工具的手段来保证项目的维护性和增强代码的可读性。React 源码在 ES2015 的基础上,除了用 ESLint 保证代码风格之外,也引入了 Flow 做静态类型检查。之所以选择 Flow,最根本原因应该和 Vue 一样,还是在于工程上成本和收益的考量。 大致体现在以下几点:

第一点:使用 Flow 可以一个一个文件地迁移,如果使用 TypeScript,则需要全部替换,成本极高,短期内并不现实;
第二点:Babel 和 ESLint 都有对应的 Flow 插件以支持语法,可以完全沿用现有的构建配置,非常小成本的改动就可以拥有静态类型检查的能力;
第三点:更贴近 ES 规范。除了 Flow 的类型声明之外,其他都是标准的 ES。万一哪天不想用 Flow 了,用babel-plugin-transform-flow-strip-types 转一下,就得到符合规范的 ES;
第四点:在需要的地方保留 ES 的灵活性,并且对于生成的代码尺寸有更好的控制力 (rollup / 自定义 babel 插件)。

如何用静态类型检查工具 Flow

在这里我们就简单说一说 Flow 的用法,其他用法可以参考Flow官网(可能需要 VPN,非常不稳定),有时间我会详细写一篇 Flow 使用指南。

Flow 仅仅是一个用于检查的工具,安装使用都很方便,使用时注意以下3点即可:

1.将 Flow 安装到我们的项目中。
2.确保编译之后的代码移除了 Flow 相关的语法。
3.在需要检查的地方增加了 Flow 相关的类型注解。

第一点:将Flow增加到我们的项目中

安装最新版本的 Flow:

$ npm install --save-dev flow-bin

安装完成之后在 package.json 文件中增加执行脚本:

{
  // ...
  "scripts": {
    "your-script-name": "flow",
    // ...
  },
  // ...
}

然后初始化 Flow:

$ npm run flow init

执行完成后,Flow 会在终端输出以下内容:

> yourProjectName@1.0.0 flow /yourProjectPath
> flow "init"

然后在根目录下生成一个名为 .flowconfig 的文件,打开之后是这样的:

[ignore]

[include]

[libs]

[lints]

[options]

[strict]

基本上,配置文件没有什么特殊需求是不用去配置的,Flow 默认涵盖了当前目录之后的所有文件。[include] 用于引入项目之外的文件。例如:

[include]

../otherProject/a.js

[libs]

它会将和当前项目平级的 otherProject/a.js 文件纳入进来。详细配置文件请看官网

第二点:编译之后的代码移除 Flow 相关的语法

Flow 在 JavaScript 语法的基础上使用了一些注解(annotation)进行了扩展。因此浏览器无法正确的解读这些 Flow 相关的语法,我们必须在编译之后的代码中(最终发布的代码)将增加的 Flow 注解移除掉。具体方法需要看我们使用了什么样的编译工具。下面将说明一些 React 开发常用的编译工具:

方式一:create-react-app

如果你的项目是使用create-react-app直接创建的,那么移除 Flow 语法的事项就不用操心了,create-react-app 已经帮你搞定了这个事。

方式二:Babel

如果使用 Babel 我们需要安装一个 Babel 对于 Flow 的 preset:

$ npm install --save-dev babel-preset-flow

然后,我们需要在项目根目录Babel 的配置文件 .babelrc中添加一个 Flow 相关的 preset:

{
  "presets": [
    "flow",
    //other config
  ]
}

方式三:flow-remove-types

如果你既没有使用 create-react-app 也没使用 Babel 作为语法糖编译器,那么可以使用 flow-remove-types 这个工具在发布之前移除 Flow 代码。

第三点:在需要检查的地方增加 Flow 相关的类型注解

如果你了解 C++/C# 的元编程或者 Java 的 Annotation,那么理解 Flow 的 Annotation 就会非常轻松。大概就是在文件、方法、代码块之前增加一个注解(Annotation)用来告知 Flow 的执行行为。

首先,Flow 只检查包含// @flow注解的文件,所以如果需要检查,我们需要这样编写我们的文件,首先我们写一个正确的示例:

/* @flow */

function add(x: number, y: number): number {
  return x + y
}

add(22, 11)

运行 Flow 终端会打印出以下内容:

> yourProjectName@1.0.0 flow /yourProjectPath
> flow "init"

Found 0 errors

承接上面代码,我们把代码修改成带有检查错误的例子:

/* @flow */

function add(x: number, y: number): number {
  return x + y
}

add("Hello", 11)

运行 Flow 终端会打印出以下内容:

> yourProjectName@1.0.0 flow /yourProjectPath
> flow "init"

Error ------------------------------------------------------------------------------------- src/platforms/web/mnr.js:8:5

Cannot call `add` with `"Hello"` bound to `x` because string [1] is incompatible with number [2].

   src/platforms/web/mnr.js:8:5
   8| add("Hello", 11)
          ^^^^^^^ [1]

References:
   src/platforms/web/mnr.js:4:17
   4| function add(x: number, y: number): number {
                      ^^^^^^ [2]



Found 1 error

到这里,Flow 已经算是安装成功了,接下来的事是要增加各种注解以加强类型限定或者参数检测。之后的内容将简要介绍 flow 的类型检查方式。

Flow 的类型检查方式

现在我们就说说 Flow 常用的2种类型检查方式:
类型推断:通过变量的执行上下文来推断出变量类型,然后根据这些推断来检查类型。
类型注释:事先注释好我们期望的类型,Flow 会基于这些注释来检查。

第一种方式:类型推断

此方式不需要编写任何代码即可进行类型检查,最小化开发者的工作量,它也不会强制你改变开发习惯,因为它会自动推断出变量的类型,这就是所谓的类型推断,Flow 最重要的特性之一。

通过一个简单例子说明一下:

/*@flow*/

function split(str) {
  return str.split(' ')
}

split(11)

Flow 检查上述代码后会报错,因为函数 split 期待的参数是字符串,而我们输入的是数字。

第二种方式:类型注释

如上所述,类型推断是 Flow 最有用的特性之一,不需要编写任何代码就能进行类型检查。但在某些特定的场景下,使用类型注释可以提供更好更明确的检查依据。

看看以下代码:

/*@flow*/

function add(x, y){
  return x + y
}

add('Hello', 11)

Flow 根据类型推断检查上述代码时检查不出任何错误,因为从语法层面考虑, + 既可以用在字符串上,也可以用在数字上,我们并没有明确指出 add() 的参数必须为数字。在这种情况下,我们可以借助类型注释来指明期望的类型。类型注释是以冒号 : 开头,可以在函数参数,返回值,变量声明中使用。如果我们在上段代码中使用类型注释,就会变成如下:

/*@flow*/

function add(x: number, y: number): number {
  return x + y
}

add('Hello', 11)

现在 Flow 就能检查出错误,因为函数参数的期待类型为数字,而我们提供了字符串。上面的例子是针对函数的类型注释。接下来我们来看看 Flow 能支持的一些常见的类型注释:

第一种:数组

/*@flow*/

var arr: Array<number> = [1, 2, 3]

arr.push('Hello')

数组类型注释的格式是 Array<T>,T 表示数组中每项的数据类型。在上述代码中,arr 是每项均为数字的数组。如果我们给这个数组添加了一个字符串,Flow 能检查出错误。

第二种:类和对象

/*@flow*/

class Bar {
  x: string;           // x 是字符串
  y: string | number;  // y 可以是字符串或者数字
  z: boolean;

  constructor(x: string, y: string | number) {
    this.x = x
    this.y = y
    this.z = false
  }
}

var bar: Bar = new Bar('hello', 4)

var obj: { a: string, b: number, c: Array<string>, d: Bar } = {
  a: 'hello',
  b: 11,
  c: ['hello', 'world'],
  d: new Bar('hello', 3)
}

类的类型注释格式如上,可以对类自身的属性做类型检查,也可以对构造函数的参数做类型检查。这里需要注意的是:属性 y 的类型中间用 | 做间隔,表示 y 的类型即可以是字符串也可以是数字。

对象的注释类型类似于类,需要指定对象属性的类型。

第三种:Null/undefined

Flow 会检查所有的 JavaScript 基础类型—— Boolean、String、Number、null、undefined(在Flow中用void代替)。除此之外还提供了一些操作符号,例如 text : ?string,它表示参数存在“没有值”的情况,除了传递 string 类型之外,还可以是 null 或 undefined。需要特别注意的是,这里的没有值和 JavaScript 的表达式的“非”是两个概念,Flow 的“没有值”只有 null、void(undefined),而 JavaScript 表达式的“非”包含:null、undefined、0、false。

如果想任意类型 T 可以为 null 或者 undefined,只需写成如下 ?T 的格式即可:

/*@flow*/

var foo: ?string = null

此时,foo 可以为字符串,也可以为 null。

Flow 在 React 源码中的应用

Flow 是 Facebook 开源的静态代码检查工具,它的作用就是在运行代码之前对 React 组件以及 Jsx 语法进行静态代码的检查以发现一些可能存在的问题。在 React v16 Fiber中的部分 TypeScript 代码只是类型声明文件和测试代码,也就是为了方便利用 TypeScript 写应用的开发者使用 React,给了接口定义和测试样例而已。

小结

通过对 Flow 的认识,有助于我们阅读 React 的源码,这种静态类型检查的方式非常有利于大型项目源码的开发和维护。此外,通过 React 重构,我们发现项目重构要么依赖规范,要么就得自己有绝对控制权,同时还要考量开发成本、项目收益以及整个团队的技术水平,并不是一味的什么火就用什么。

Rollup - 另一个前端模块化的打包工具

Rollup 是前端模块化的一个打包工具,可以将小块代码编译成大块复杂的代码,例如 library 或应用程序。简单地说,它可以从一个入口文件开始,将所有使用的模块根据命令或者根据 Rollup 配置文件打包成一个目标文件,并且 Rollup 会自动过滤掉那些没有被使用过的函数或变量,从而使代码最小化,如果想使用直接导入这一个目标文件即可,因此 Rollup 极其适合构建一个工具库。

这里提到 Rollup 的两个特别重要的特性,第一个就是它使用了 ES2015 的模板标准,这意味着你可以直接使用 import 和 export 而不需要引入 babel。另一个重要特性叫做 tree-shaking,这个特性可以帮助你将无用代码(即没有使用的代码)从最终的目标文件中过滤掉。举个简单的例子,我们在 foo.js 文件定义了 f1 和 f2 两个方法,然后在入口文件 index.js 只引入了 foo.js 文件中的 f1 方法,那么在最后打包 index.js 文件时,Rollup 就不会将 f2 方法打包到最终文件中(这个特性是基于 ES6 模块的静态分析的,也就是说,只有 export 而没有 import 的变量是不会被打包到最终代码中的)。

为什么用前端模块化的打包工具 Rollup

之前的构建系统是基于 Gulp/Grunt+Browserify 手搓的一套工具,后来在扩展方面受限于工具,例如:

Node 环境下性能不好:频繁的process.env.NODE_ENV访问拖慢了SSR 性能,但又没办法从类库角度解决,因为Uglify依靠这个去除无用代码,所以React SSR性能最佳实践一般都有一条“重新打包 React,在构建时去掉 process.env.NODE_ENV”.

丢弃了过于复杂(overly-complicated)的自定义构建工具,改用更合适的 Rollup:

It solves one problem well: how to combine multiple modules into a flat file with minimal junk code in between.

无论 Haste -> ES Module 还是 Gulp/Grunt+Browserify -> Rollup 的切换都是从非标准的定制化方案切换到标准的开放的方案,应该在“手搓”方面吸取教训,为什么业界规范的东西在我们的场景不适用,非要自己造吗?

如何用前端模块化的打包工具 Rollup

关于如何使用前端模块化的打包工具 Rollup,这里就不做过多介绍了,可参考我之前写的一篇文章:Rollup使用指南,更详细的使用文档可参考:官网

Webpack 和 Rollup 有什么不同

2017年4月初,Facebook 将一个巨大的 pull 请求合并到了 React 主分支(master)中,将其现有的构建流程替换为基于 Rollup,这一举动促使一些人产生很大的疑惑“React 为什么选择 Rollup 而抛弃 webpack”,难道webpack要跌下神坛了?

Webpack 是目前使用最为火热的打包工具,没有之一,每月有数百万的下载量,为成千上万的网站和应用提供支持。相比之下,Rollup 并不起眼。但 React 并不孤单 – Vue,Ember,Preact,D3,Three.js,Moment 以及其他许多知名的库也使用 Rollup 。世界到底怎么了?为什么我们不能只有一个大众认可的 JavaScript 模块化打包工具?

Webpack 始于2012年,由 Tobias Koppers 发起,用于解决当时现有工具未解决的的一个难题:构建复杂的单页应用程序(SPA)。特别是 webpack 的两个特性改变了一切:

第一个特性:代码拆分(Code Splitting)

代码拆分也就是说你可以将应用程序分解成可管理的代码块,可以按需加载,这意味着用户可以快速获取网站内容,而不必等到整个应用程序下载和解析完成。

第二个特性:各式各样的加载器(loader)

不管是图像,css,还是 html ,在 Webpack 看来一切都可作为模块,然后通过不同的加载器 loader 来加载它们。

ES6 发布之后,其中引入的模块机制使得静态分析成为了可能,于是 Rollup 发布了:其中 Rollup 有两个特别重要的特性,第一个就是它利用 ES2015 巧妙的模块设计,尽可能高效的构建出能够直接被其他 Javascript 库的。另一个重要特性叫做 tree-shaking,这个特性可以帮助你将无用代码(即没有使用的代码)从最终的目标文件中过滤掉。

紧接着 Webpack2 发布,仿照 Rollup 增加了 tree-shaking。 在之后, Webpack3 发布,仿照 Rollup 又增加了 Scope Hoisting。在在之后, Parcel 发布了一个快速、零配置的打包工具。于是,Webpack4 仿照 Parcel 发布了。

说了这么多,工作中我们到底该用哪个工具?

对于应用使用 webpack,对于类库使用 Rollup。如果你需要代码拆分(Code Splitting),或者你有很多静态资源需要处理,再或者你构建的项目需要引入很多 CommonJS 模块的依赖,那么 webpack 是个很不错的选择。如果您的代码库是基于 ES2015 模块的,而且希望你写的代码能够被其他人直接使用,你需要的打包工具可能是 Rollup。

小结

无论 Haste -> ES Module 还是 Gulp/Grunt+Browserify -> Rollup 的切换都是从非标准的定制化方案切换到标准的开放的方案,可以看出 React 团队也在积极拥抱标准方案并非一味造轮子。其实 Vue.js 1.0.10 就已经使用 Rollup 了,而 React v16.0 改用 Rollup 肯定也有借鉴之意,因此,好技术都是在借鉴的大背景下诞生的(Vue 就是一个典型的例子)。在这里通过对 Rollup 的认识,有助于我们了解 React 的构建以及源码目录结构。

查看原文

赞 16 收藏 11 评论 0

hasaki 评论了文章 · 2018-07-23

vue、react等单页面项目应该这样子部署到服务器

服务端渲染:next.js、nuxt.js等服务端渲染框架构建的项目部署到服务器,并用PM2守护程序

最近好多伙伴说,我用vue做的项目本地是可以的,但部署到服务器遇到好多问题:资源找不到直接访问index.html页面空白刷新当前路由404。。。用react做的项目也同样遇到类似问题。现在我们一起讨论下单页面如何部署到服务器?

由于前端路由缘故,单页面应用应该放到nginx或者apache、tomcat等web代理服务器中,千万不要直接访问index.html,同时要根据自己服务器的项目路径更改react或vue的路由地址。

如果说项目是直接跟在域名后面的,比如:http://www.sosout.com ,根路由就是 '/'。
如果说项目是直接跟在域名后面的一个子目录中的,比如:http://www.sosout.com/children ,根路由就是 '/children ',不能直接访问index.html。

以配置Nginx为例,配置过程大致如下:(假设:
1、项目文件目录: /mnt/html/spa(spa目录下的文件就是执行了npm run dist 后生成的dist目录下的文件)
2、访问域名:spa.sosout.com
进入nginx.conf新增如下配置:

server {
    listen 80;
    server_name  spa.sosout.com;
    root /mnt/html/spa;
    index index.html;
    location ~ ^/favicon\.ico$ {
        root /mnt/html/spa;
    }

    location / {
        try_files $uri $uri/ /index.html;
        proxy_set_header   Host             $host;
        proxy_set_header   X-Real-IP        $remote_addr;
        proxy_set_header   X-Forwarded-For  $proxy_add_x_forwarded_for;
        proxy_set_header   X-Forwarded-Proto  $scheme;
    }
    access_log  /mnt/logs/nginx/access.log  main;
}

注意事项:
1、配置域名的话,需要80端口,成功后,只要访问域名即可访问的项目
2、如果你使用了react-router的 browserHistory 模式或 vue-router的 history 模式,在nginx配置还需要重写路由:

server {
    listen 80;
    server_name  spa.sosout.com;
    root /mnt/html/spa;
    index index.html;
    location ~ ^/favicon\.ico$ {
        root /mnt/html/spa;
    }

    location / {
        try_files $uri $uri/ @fallback;
        index index.html;
        proxy_set_header   Host             $host;
        proxy_set_header   X-Real-IP        $remote_addr;
        proxy_set_header   X-Forwarded-For  $proxy_add_x_forwarded_for;
        proxy_set_header   X-Forwarded-Proto  $scheme;
    }
    location @fallback {
        rewrite ^.*$ /index.html break;
    }
    access_log  /mnt/logs/nginx/access.log  main;
}

为什么要重写路由?因为我们的项目只有一个根入口,当输入类似/home的url时,如果找不到对应的页面,nginx会尝试加载index.html,这是通过react-router或vue-router就能正确的匹配我们输入的/home路由,从而显示正确的home页面,如果browserHistory模式或history模式的项目没有配置上述内容,会出现404的情况。

简单举两个例子,一个vue项目一个react项目:

vue项目:

域名:http://tb.sosout.com

clipboard.png

import App from '../App'

// 首页
const home = r => require.ensure([], () => r(require('../page/home/index')), 'home')

// 物流
const logistics = r => require.ensure([], () => r(require('../page/logistics/index')), 'logistics')

// 购物车
const cart = r => require.ensure([], () => r(require('../page/cart/index')), 'cart')

// 我的
const profile = r => require.ensure([], () => r(require('../page/profile/index')), 'profile')

// 登录界面
const login = r => require.ensure([], () => r(require('../page/user/login')), 'login')

export default [{
  path: '/',
  component: App, // 顶层路由,对应index.html
  children: [{
    path: '/home', // 首页
    component: home
  }, {
    path: '/logistics', // 物流
    component: logistics,
    meta: {
      login: true
    }
  }, {
    path: '/cart', // 购物车
    component: cart,
    meta: {
      login: true
    }
  }, {
    path: '/profile', // 我的
    component: profile
  }, {
    path: '/login', // 登录界面
    component: login
  }, {
    path: '*',
    redirect: '/home'
  }]
}]

clipboard.png

############
# 其他配置
############

http {
    ############
    # 其他配置
    ############
    server {
        listen 80;
        server_name  tb.sosout.com;
        root /mnt/html/tb;
        index index.html;
        location ~ ^/favicon\.ico$ {
            root /mnt/html/tb;
        }
    
        location / {
            try_files $uri $uri/ @fallback;
            index index.html;
            proxy_set_header   Host             $host;
            proxy_set_header   X-Real-IP        $remote_addr;
            proxy_set_header   X-Forwarded-For  $proxy_add_x_forwarded_for;
            proxy_set_header   X-Forwarded-Proto  $scheme;
        }
        location @fallback {
            rewrite ^.*$ /index.html break;
        }
        access_log  /mnt/logs/nginx/access.log  main;
    }
    ############
    # 其他配置
    ############   
}

react项目:

域名:http://antd.sosout.com

clipboard.png

/**
* 疑惑一:
* React createClass 和 extends React.Component 有什么区别?
* 之前写法:
* let app = React.createClass({
*      getInitialState: function(){
*        // some thing
*      }
*  })
* ES6写法(通过es6类的继承实现时state的初始化要在constructor中声明):
* class exampleComponent extends React.Component {
*    constructor(props) {
*        super(props);
*        this.state = {example: 'example'}
*    }
* }
*/

import React, {Component, PropTypes} from 'react'; // react核心
import { Router, Route, Redirect, IndexRoute, browserHistory, hashHistory } from 'react-router'; // 创建route所需
import Config from '../config/index';
import layout from '../component/layout/layout'; // 布局界面

import login from '../containers/login/login'; // 登录界面

/**
 * (路由根目录组件,显示当前符合条件的组件)
 * 
 * @class Roots
 * @extends {Component}
 */
class Roots extends Component {
    render() {
        // 这个组件是一个包裹组件,所有的路由跳转的页面都会以this.props.children的形式加载到本组件下
        return (
            <div>{this.props.children}</div>
        );
    }
}

// const history = process.env.NODE_ENV !== 'production' ? browserHistory : hashHistory;

// 快速入门
const home = (location, cb) => {
    require.ensure([], require => {
        cb(null, require('../containers/home/homeIndex').default)
    }, 'home');
}

// 百度图表-折线图
const chartLine = (location, cb) => {
    require.ensure([], require => {
        cb(null, require('../containers/charts/lines').default)
    }, 'chartLine');
}

// 基础组件-按钮
const button = (location, cb) => {
    require.ensure([], require => {
        cb(null, require('../containers/general/buttonIndex').default)
    }, 'button');
}

// 基础组件-图标
const icon = (location, cb) => {
    require.ensure([], require => {
        cb(null, require('../containers/general/iconIndex').default)
    }, 'icon');
}

// 用户管理
const user = (location, cb) => {
    require.ensure([], require => {
        cb(null, require('../containers/user/userIndex').default)
    }, 'user');
}

// 系统设置
const setting = (location, cb) => {
    require.ensure([], require => {
        cb(null, require('../containers/setting/settingIndex').default)
    }, 'setting');
}

// 广告管理
const adver = (location, cb) => {
    require.ensure([], require => {
        cb(null, require('../containers/adver/adverIndex').default)
    }, 'adver');
}

// 组件一
const oneui = (location, cb) => {
    require.ensure([], require => {
        cb(null, require('../containers/ui/oneIndex').default)
    }, 'oneui');
}

// 组件二
const twoui = (location, cb) => {
    require.ensure([], require => {
        cb(null, require('../containers/ui/twoIndex').default)
    }, 'twoui');
}

// 登录验证
const requireAuth = (nextState, replace) => {
    let token = (new Date()).getTime() - Config.localItem('USER_AUTHORIZATION');
    if(token > 7200000) { // 模拟Token保存2个小时
        replace({
            pathname: '/login',
            state: { nextPathname: nextState.location.pathname }
        });
    }
}

const RouteConfig = (
    <Router history={browserHistory}>
        <Route path="/home" component={layout} onEnter={requireAuth}>
            <IndexRoute getComponent={home} onEnter={requireAuth} /> // 默认加载的组件,比如访问www.test.com,会自动跳转到www.test.com/home
            <Route path="/home" getComponent={home} onEnter={requireAuth} />
            <Route path="/chart/line" getComponent={chartLine} onEnter={requireAuth} />
            <Route path="/general/button" getComponent={button} onEnter={requireAuth} />
            <Route path="/general/icon" getComponent={icon} onEnter={requireAuth} />
            <Route path="/user" getComponent={user} onEnter={requireAuth} />
            <Route path="/setting" getComponent={setting} onEnter={requireAuth} />
            <Route path="/adver" getComponent={adver} onEnter={requireAuth} />
            <Route path="/ui/oneui" getComponent={oneui} onEnter={requireAuth} />
            <Route path="/ui/twoui" getComponent={twoui} onEnter={requireAuth} />
        </Route>
        <Route path="/login" component={Roots}> // 所有的访问,都跳转到Roots
            <IndexRoute component={login} /> // 默认加载的组件,比如访问www.test.com,会自动跳转到www.test.com/home
        </Route>
        <Redirect from="*" to="/home" />
    </Router>
);

export default RouteConfig;

clipboard.png

############
# 其他配置
############

http {
    ############
    # 其他配置
    ############
    server {
        listen 80;
        server_name  antd.sosout.com;
        root /mnt/html/reactAntd;
        index index.html;
        location ~ ^/favicon\.ico$ {
            root /mnt/html/reactAntd;
        }

        location / {
            try_files $uri $uri/ @router;
            index index.html;
            proxy_set_header   Host             $host;
            proxy_set_header   X-Real-IP        $remote_addr;
            proxy_set_header   X-Forwarded-For  $proxy_add_x_forwarded_for;
            proxy_set_header   X-Forwarded-Proto  $scheme;
        }
        location @router {
            rewrite ^.*$ /index.html break;
        }
        access_log  /mnt/logs/nginx/access.log  main;
    }

    ############
    # 其他配置
    ############   
}
查看原文

hasaki 回答了问题 · 2018-07-06

laravel5.3 + vue 请问vue的config 文件在哪里?

进入根目录下server.dev.js文件,搜索proxyTable就找到了。截图如下:

clipboard.png

关注 3 回答 2

hasaki 回答了问题 · 2018-07-05

解决nginx 服务器 还是apache服务器好 还是说 nginx +apache。。

nginx的优点:
1、轻量,配置简洁
2、异步的,多个连接(万级别)可以对应一个进程
3、nginx适合做静态请求,简单,效率高,占用更少的内存及资源,前端一般用nginx作为反向代理

apache的优点:
1、配置复杂,但rewrite强大
2、同步多进程模型,一个连接对应一个进程
3、apache适合处理动态请求,稳定,功能强

一般来说,需要性能的web 服务,用nginx。如果不需要性能只求稳定,那就apache吧!现在好多集群站,前端nginx抗并发,后端apache集群,配合一起使用!至于好与不好,只是相对的。

关注 7 回答 7

hasaki 回答了问题 · 2018-06-19

做一个类似思否但业务更加复杂的论坛,前端选择什么框架比较好?

论坛类型网站一般要求对SEO友好!如果您们客户对兼容性要求不高,可以考虑 react版本的 nextjs 或 vue版本的 nuxtjs ,相反您们对浏览器兼容性要求比较高,可以考虑用nodejs做中间层(这样做的好处就是你们大服务端如java,php等保持分离),最坏的打算就是用服务端加模板引擎,比如我们常见的java + JSP。

其他推荐方案:Backbone、Angular、Avalon等

关注 4 回答 3

hasaki 回答了问题 · 2018-06-06

解决引入vux,v-chart 报错

谢邀!
vux2必须配合vux-loader使用, 请在build/webpack.base.conf.js里参照如下代码进行配置:

const vuxLoader = require('vux-loader')
const webpackConfig = originalConfig // 原来的 module.exports 代码赋值给变量 webpackConfig

module.exports = vuxLoader.merge(webpackConfig, {
  plugins: ['vux-ui']
})

具体参考:安装使用(webpack)

关注 6 回答 4

hasaki 回答了问题 · 2018-04-26

解决vue-cli(3.0.0-beta.6)创建项目后使用yarn serve就报错

首先vue-cli3.0目前是公测阶段,个人不建议不要用于实际项目,等到正式版再使用!文档里也有说明:
Most of the planned features are in place but there may still be bugs. API may still change until we reach RC phase. Do not use in production yet unless you are adventurous.
大致意思:我们大致功能已经开发完毕,但仍可能存在漏洞。在发布候选版(RC)之前,API也可能会改变。除非你有冒险精神,否则不要在生产中使用。

回到您的问题,上面的已经提供了一些解决办法:
比如降版本,在package.json里面的devDependencies添加@babel/preset-stage-2 :

"devDependencies": {
    "@babel/preset-stage-2": "7.0.0-beta.44",
    ...
}

或者github的其他解决方案:npm run serve error: You must pass the "decoratorsLegacy": true option to @babel/preset-stage-2

更或者升级版本,在今天vue-cli发布了3.0.0-beta.7(推荐)

关注 5 回答 4

hasaki 回答了问题 · 2018-04-19

打包npm组件模块如何抽取公用库让其他安装这个组件的使用自己的共用库?

官方英文文档
如果英文不好,可以参考以下两篇文章:
webpack之深入浅出externals
webpack externals 深入理解

关注 2 回答 2

hasaki 回答了问题 · 2018-04-19

解决react 项目中使用了typescript,是否还需要使用prop-types来进行格式验证。

可以只用typescript进行验证,也可以只用prop-types进行验证,也可以两者混搭进行验证,这个并没用严格限制。
首先不管用typescript,还是prop-types,都只在开发模式下进行检查,typescript无需多言,react我们参考官网:
clipboard.png
只是用typescript验证提示是ts语言带来的功能,用prop-types验证提示是react带来的功能。至于写法我们可以参考官网(官网真是好东西):

clipboard.png

clipboard.png
typescript参考链接

react参考链接

如果想看中文的,请自行百度(感冒,睡不着,也不想工作)。

最后我们拿当下最火的ui框架ant-design,咱们看看阿里的源码怎么写的?

Affix组件(两者都有用):

clipboard.png

Alert组件(只用了一种):

clipboard.png

关注 4 回答 2

hasaki 回答了问题 · 2018-04-18

解决react为什么用class和箭头函数效果是一样的

谈到组件,我们就有必要谈谈 React 组件 的三种写法:

1、ES5-写法 React.createClass(原始的createClass写法)

早期React使用该方法来构建一个组件,它接受一个对象为参数,对象中必须声明一个render方法,render返回一个组件实例,现阶段已不推荐使用createClass方法来创建组件,基于es5,加上臃肿,this自绑定导致性能略差,注定是要被淘汰的。大致代码如下:

var React = require('react');
var ReactDOM = require('react-dom');
var MyComponent = React.createClass({
    getDefaultProp:function() {
        return {};
    },
    getInitialState: function() {
        return {};
    },
    render: function() {
        return (
            <div>ES5-写法 React.createClass</div>
        );
    }
 });

2、ES6-写法 React.Component

从v0.13版本,官方就推荐用 ES6 的class语法创建有状态组件,也就是我们可以使用class MyComponent extends React.Component{...}的方式创建组件,大致代码如下:

class MyComponent extends React.Component {
    constructor(props) {
        super(props)
        this.state = {}
    }
    render() {
        return (
            <div>ES6-写法 React.Component</div>
        )
    }
}

React.Component的特点:
(1)、React.createClass通过getInitialState函数初始state,而React.Component是在constructor中直接声明state的。
(2)、React.createClass的this是自动绑定的,而React.Component需要手动绑定。

3、无状态的函数式写法(纯组件 SFC)

React.createClass和React.Component都可以用来创建有状态的组件,而 v0.14 之后官方提出了无状态组件 - Stateless Component。该写法不需要管理状态state,数据直接通过props传入,这也符合 React 单向数据流的思想,同时提高代码的可读性和大大减少代码量,大致代码如下:

const MySFC= (props) => (
    <div>
        // props就是要展示的数据
    </div>
);

无状态组件的特点:
(1)、组件不会被实例化,整体渲染性能得到提升
(2)、组件无法访问生命周期的方法

关注 4 回答 4

hasaki 回答了问题 · 2018-04-13

解决vue.js dev 警告:There are multiple modules...

以上错误信息,关键点有三句:

1、There are multiple modules with names that only differ in casing.有多个模块同名仅大小写不同

2、This can lead to unexpected behavior when compiling on a filesystem with other case-semantic.这可能导致在一些文件系统中产生不是预期的行为

Use equal casing.使用唯一的写法

linux和mac是严格区分大小写的,而windows不区分。

关注 2 回答 2