Git Product home page Git Product logo

blog's People

Contributors

fengliner avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

Forkers

apollotang

blog's Issues

【译文】Yarn 的确定性

原文https://yarnpkg.com/blog/2017/05/31/determinism/

Yarn 提出的其中一个观点是它让包管理变成确定性的。但这到底意味着什么呢?这篇博文突出说明了 yarn
和 npm 5是如何保证包管理是确定性的,并且指出它们提供的确切保证和做出选择的权衡点的不同。

什么是确定性

在 Javascript 包管理的背景下,确定性是指在给定的 package.json 和 lock 文件下始终能得到一致的 node_modules 目录结构。

Yarn 确定性的保证因素

Lockfile

只要使用相同的 yarn 版本,yarn 就是完全确定性的。在 yarn 和 npm 5 中,确定性是由 lockfile 文件确保的,lockfile 包含了整个依赖树的信息。然而,在 yarn 和 npm 5中,lockfile 的文件格式是不同的。我们从来没有公开谈论过为什么使用这种格式,这里我们会带你一起看一下。

如果你曾经看过 yarn.lock 文件,你应该对下面的这种结构感到很熟悉。

has-flag@^1.0.0:
  version "1.0.0"
  resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-1.0.0.tgz#9d9e793165ce017a00f00418c43f942a7b1d11fa"

supports-color@^3.2.3:
  version "3.2.3"
  resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.2.3.tgz#65ac0504b3954171d8a64946b2ae3cbb8a5f54f6"
  dependencies:
    has-flag "^1.0.0"

这是通过运行命令 yarn add supports-color而产生的 yarn.lock 文件。这个文件包含了 supports-color 和 它的子依赖 has-flag 的确切版本。

使用这个 lockfile 文件,我们可以确定 supports-color 依赖的 has-flag 总是使用相同的版本。

但是 lockfile 文件缺少一个关键的信息,那就是依赖树上的每一个依赖的 提升(hoisting) 和 位置。比如,给定一个 yarn.lock 文件是不可能确定顶层依赖的,除非同时拥有 package.json 文件。即使知道了顶层依赖,我们仍然无法推断出应该依赖装在什么位置。

实际上,这意味着在 node_modules 目录中 package 的位置是在 yarn 的内部计算出来的。这就使得在使用不同版本的 yarn 时可能会引起不确定性。

我们这样做的原因是 这种 lockfile 文件的格式对 比对(diffing) 是非常有用的。也就是说,对 lockfile 的修改很容易被人审查。我们使用自定义格式而不是JSON,并将所有内容都放在顶层,这样便于阅读和审查。合并冲突通常由版本控制自动处理,这样就可以减少冲突。

提升保证

尽管在不同的版本中,yarn 提升可能会有所不同。但在使用相同版本的 yarn 时,我们仍然会对提升进行强有力的保证。这些保证中最重要的是忽略诸如 optionalDependencies 和 devDependencies 这样的会影响正常依赖关系的位置的环境依赖。

这实在有些让人难以理解,那到底是什么意思呢?

在 package.json 文件里你可以声明多种类型的依赖。其中两个,一个是始终会安装的简单依赖,另一个是只有在 package.json 目录下运行 npm installyarn 才会安装的 dev 依赖。

由于这些特性,在忽略了某些依赖后可能会导致不同的 node_modules 目录结构。但是在确定依赖在 node_modules 中的确定位置时,yarn 仍然会把所有的依赖考虑在内。即使有些依赖没有被安装,他们仍然会影响其他依赖的提升位置。这一点很重要,因为在生产和开发环境中,包的提升位置有差异会导致非常奇怪的 bug。

注:这个保证不是 yarn 唯一的,npm 5 也是这样做的。

和 npm 5 比较

npm 5 引入了一个称为 package-lock 的类似 shrinkwrap 特性的 设计。package-lock 文件包含了创建node_modules 所需的所有信息以及所有的提升信息。先前的 yarn.lock 文件的 npm 版本是如下的结构:

{
  "name": "react-example",
  "version": "1.0.0",
  "lockfileVersion": 1,
  "dependencies": {
    "has-flag": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz",
      "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo="
    },
    "supports-color": {
      "version": "3.2.3",
      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz",
      "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY="
    }
  }
}

注意,在第一个 dependencies 对象里列出的所有依赖都是提升的。这就意味着 npm 仅仅使用 package-lock 文件就可以确定最终的依赖关系图,而 yarn 却需要相应的 package.json 文件。

这意味着 npm 可以更好地保证在 不同的 npm 版本中提升位置,而代价是拥有一个更密集的锁文件。对于 yarn 来说,关于 lockfile 的互操作性还不是很清楚,所以目前还没有关于如何支持 package-lock 的计划。但是,你可以想象在将来 yarn 会同时支持和更新他们。我们对社区的反馈非常感兴趣,并鼓励人们就如何将其作为RFC提出建议。

小结

每个 lock 文件格式都有不同的考虑方案,而且似乎没有完美的格式。在决定使用什么包管理器时,评估你需要什么样的保证是很重要的。

npm 5 具有更强的跨版本的保证,并且具有更强的确定性锁文件。但是只有当你使用同一个版本时,yarn 才有这些保证,因为这样会更有利于一个更轻量的锁文件以及更好的审查。很可能会有一个更好的 lockfile 的解决方案,但是 yarn 和 npm 5 的锁文件代表了当前的状态,并且可能在未来发生融合。

希望这篇文章让您了解 yarn 和 npm 之间的确定性保证的不同,并帮助您决定什么对您的公司或项目更有效。

浅谈 TypeScript 中的代码检查

代码检查主要是用来发现代码中的错误,统一代码风格。代码检查可以让无强约束的规范变成强约束,编写不规范的代码变得困难,极大地提高代码的规范化和可维护性。在 JavaScript 项目中,一般使用 ESlint 进行代码检查。ESlint 提供了丰富的规则和插件,可以很方便地检查 JavaScript 代码。那么在 TypeScript 项目中可以使用 ESlint 检查ts代码吗?答案是可以。目前 TypeScript 的代码检查主要有以下三种方案:TSLintESlintTypeScript-ESLint

TSLint

说到 TypeScript 的代码检查,想必大家第一时间想到的肯定是 TSLint。TSLint 是专门用来检查ts代码的代码检查工具,通过配置 tsconfig.json 就可以检查项目中ts代码。TSLint 与 ESLint 类似,不过除了能检查常规的 js 代码风格之外,TSLint 还能够通过 TypeScript 的语法解析,利用类型系统做一些 ESLint 做不到的检查。比如下面的一段代码:

const a = 0;
const b = '0';

if (a === b) {
  console.log('bingo!');
}

对于 if 条件中的判断 a === b,tslint 可以检查出变量 a 和 变量 b 的类型,判断出这里的 if 条件永远都是 false。ESLint 无法知道 === 两边的类型,所以对这种规则无能为力。

TSLint 提供了 151 条基础规则,基本可以满足项目的需求。除此之外,TSLint 还支持用户自定义规则,可以对基础规则未涵盖的一些代码行为进行检查。每个团队可能都会有适合自己的代码规范和约定,编写适合自己团队的规则可以使无强约束的规范变成强约束,让编写不规范的代码都变得困难。过去一年,我们在自己的项目中已经添加了10+条规则,每条规则都会防止不规范的代码提交,并可以自动修复不规范的代码,极大地提高了团队代码质量。对于如何编写自定义的 TSLint 规则,官方文档 Developing TSLint rules 给出了一些指导。此外,astexplorer 可以帮助你轻松拿到 ts 代码的抽象语法树即 AST 信息,通过 AST 就可以很方便地来编写你想要的代码检查规则了。

ESLint

ESLint 是用来检查 JavaScript 代码的,也可以用来检查 TypeScript 代码!ESLint 相比 TSLint,基础规则更多(249:151),社区也更繁荣,插件众多。而且从 JavaScript 切换到 TypeScript 的话,可以直接复用已经定义好的 ESLint 规则。一般在 TypeScript 中使用 ESLint 的方案会有如下两种:

方案一:tslint-eslint-rules
tslint-eslint-rules 是 TSLint 的一个插件,它允许你使用 ESLint 中的规则。只需要简单配置 tslint.json 就可以开启使用。该方案的优点是配置简单,只需安装插件就可以使用。tslint-eslint-rules 只是 ESLint rule 的ts版本,所以根本上还是利用的 TSLint 来做的代码检查,并没有真正使用 ESLint,只是让你可以享用到 TSLint 规则里缺失的一些 ESLint 规则。这种方案比较适合于已经使用了 TSLint 来做代码检查,又想引入一些 ESLint 规则的项目。

方案二: ESlint + typescript-eslint-parser
由于 ESLint 默认使用 Espree 进行语法解析,无法识别 TypeScript 的一些语法,所以需要安装 typescript-eslint-parser,替换掉默认的解析器。在 ESLint 配置文件中指定 parser:

{
  "parser": "typescript-eslint-parser"
}

在实际使用中,通常我们还需要安装 eslint-plugin-typescript,弥补一些 typescript-eslint-parser 支持性不好的规则。

{
  "parser": "typescript-eslint-parser",
  "plugins": [
    "typescript"
  ],
  "rules": {
    "typescript/rule-name": "error"
  }
}

此方案是真正使用 ESLint 来检查 TypeScript 代码,可以充分使用 ESLint 的优势,适合于打算从 JavaScript 切换到 TypeScript,但不想切换 ESLint 到 TSLint 的项目。

TypeScript-ESLint

细心的读者可能会发现,上一小节中提到的 typescript-eslint-parsereslint-plugin-typescript 的 github 上都有一行提醒 This repository has been archived by the owner. It is now read-only.,咦,还没开始就结束了吗?不用担心,接着往下看,我们会发现这两个库已经被统一迁移到 TypeScript-ESLint。为什么会做这个迁移呢?或许我们从 TypeScript 官方发布的 2019 年 RoadMap 里可以一窥究竟。TypeScript Roadmap 里指出在代码检查 linting 方面,因为 TSLint 自身架构和性能方面的问题,得益于 ESLint 更优的架构和繁荣的生态,2019 年 TypeScript 会全面拥抱 ESLint。ESLint 官方也发表文章宣告 typescript-eslint-parser 项目的诞生。

typescript-eslint-parser 项目的目的就是使 ESLint 可以检查 TypeScript 代码,并提供一系列工具和插件来帮助用户在 TypeScript 中使用 ESLint。目前该项目还在初始阶段,文档和规则还都在补充完善,有兴趣的朋友可以关注一下。

结束语

以上,目前 TypeScript 中的代码检查方案主要有三种:TSLint、ESLint、TypeScript-ESLint。那么对于一个 TypeScript 项目应该选择哪一个代码检查工具呢?

如果你正在使用TSlint,那么可以继续使用,并且通过 tslint-eslint-rules 来引入 ESLint规则。

如果你正在从 JavaScript 切换到 TypeScript,并且已经有了完善的 ESLint 规则,那么可以继续使用ESLint,配合 typescript-eslint-parser 完全可以用来检查 TypeScript 代码。

如果是一个全新的 TypeScript 项目,TypeScript-ESLint 或许已经表明了未来的方向,全面拥抱 ESLint,但是使用 TSLint 也不用担心,TypeScript-ESLint 正在准备 TSLint 到 ESLint 的迁移方案。

无论选择哪种方案,代码检查的目标永远都是为代码服务,让无强约束的规范变成强约束,使编写不规范的代码变得困难,提高代码的规范化和可维护性。

Redux架构优化之如何减少样板代码

“redux很好,但是有太多的样板代码了” 。

这是大多数使用redux的同学的吐槽点。也正因为此,才诞生了那么多基于redux的优化方案。

1. action utilities for redux: redux-actions

redux-actions 为首的 函数工具库

这类方案通常是利用一个生成action creator的函数或者生成reducer creator的函数来减少使用redux中的样板代码。

1.1 定义makeActionCreator函数

可以减少简单的action creator函数,对于复杂的(异步)action creator函数不太好处理

function makeActionCreator(type, ...argNames) {
  return function(...args) {
    let action = { type }
    argNames.forEach((arg, index) => {
      action[argNames[index]] = args[index]
    })
    return action
  }
}

const ADD_TODO = 'ADD_TODO'
const EDIT_TODO = 'EDIT_TODO'
const REMOVE_TODO = 'REMOVE_TODO'

export const addTodo = makeActionCreator(ADD_TODO, 'todo')
export const editTodo = makeActionCreator(EDIT_TODO, 'id', 'todo')
export const removeTodo = makeActionCreator(REMOVE_TODO, 'id')

1.2 定义createReducer函数

把每个switch case封装成函数,本质上还是需要对每个action type进行处理

function createReducer(initialState, handlers) {
    return function reducer(state = initialState, action) {
        if (handlers.hasOwnProperty(action.type)) {
            return handlers[action.type](state, action)
        } else {
            return state
        }
    }
}

const todosreducer = createReducer([], {
    'ADD_TODO': addTodo,
    'TOGGLE_TODO': toggleTodo,
    'EDIT_TODO': editTodo
});

1.3 redux-actions

提供API:createActions handleActionscombineActions,异步action需要结合redux-promise使用

import { createActions, handleActions, combineActions } from 'redux-actions'

const defaultState = { counter: 10 };

const { increment, decrement } = createActions({
  INCREMENT: amount => ({ amount }),
  DECREMENT: amount => ({ amount: -amount })
});

const reducer = handleActions({
  [combineActions(increment, decrement)](state, { payload: { amount } }) {
    return { ...state, counter: state.counter + amount };
  }
}, defaultState);

export default reducer;

2. framework based on redux and redux-saga: dva

基于redux+react-router+redux-saga的封装的一整套解决方案的dva或类dva,比如mirrormickey

核心方法是app.model,用于reducer、initialState、action、saga封装到一起

app.model({
  namespace: 'products',
  state: {
    list: [],
    loading: false,
  },
  subscriptions: [
    function(dispatch) {
      dispatch({type: 'products/query'});
    },
  ],
  effects: {
    ['products/query']: function*() {
      yield call(delay(800));
      yield put({
        type: 'products/query/success',
        payload: ['ant-tool', 'roof'],
      });
    },
  },
  reducers: {
    ['products/query'](state) {
      return { ...state, loading: true, };
    },
    ['products/query/success'](state, { payload }) {
      return { ...state, loading: false, list: payload };
    },
  },
});

3. Simple, scalable state management: mobx

mobx已经不是redux的优化方案了,可以说是替代方案

  • mobx将state包装成可观察对象(observable)的值,可以在state改变的时候得到更新的值
  • mobx 允许直接修改state,并且是多store,actions也是可选的

在一个地方保存state,通过注解观察state,一旦state修改组件会自动的更新

import {observable, autorun} from 'mobx';

var todoStore = observable({
    /* 一些观察的状态 */
    todos: [],

    /* 推导值 */
    get completedCount() {
        return this.todos.filter(todo => todo.completed).length;
    }
});

/* 观察状态改变的函数 */
autorun(function() {
    console.log("Completed %d of %d items",
        todoStore.completedCount,
        todoStore.todos.length
    );
});

/* ..以及一些改变状态的动作 */
todoStore.todos[0] = {
    title: "Take a walk",
    completed: false
};
// -> 同步打印 'Completed 0 of 1 items'

todoStore.todos[0].completed = true;
// -> 同步打印 'Completed 1 of 1 items'

4. High level abstraction between React and Redux: kea

基于redux、redux-saga、reselect定义logic文件,通过注解的方式引用logic文件里定义的actions、reducers

const logic = kea({
  actions: () => ({
    increment: (amount) => ({ amount }),
    decrement: (amount) => ({ amount })
  }),

  reducers: ({ actions }) => ({
    counter: [0, PropTypes.number, {
      [actions.increment]: (state, payload) => state + payload.amount,
      [actions.decrement]: (state, payload) => state - payload.amount
    }]
  }),

  selectors: ({ selectors }) => ({
    doubleCounter: [
      () => [selectors.counter],
      (counter) => counter * 2,
      PropTypes.number
    ]
  })
})

class Counter extends Component {
  render () {
    const { counter, doubleCounter } = this.props
    const { increment, decrement } = this.actions

    return <div>...</div>
  }
}

export default logic(Counter)

小结

  • redux-actions:从action层面解决redux样板代码的问题,仅仅是一个库(library)
  • dvakea:不同于redux-actions只提供一个简单的库(library),二者都是基于redux及redux的一些异步方案(redux-sagaredux-thunk等)封装的一套框架(framework)。而且二者的设计理念也很相似,都是把action、reducer、saga封装到一个logic或model里,然后把logic或model和组件connect起来进行引用。不同的是kea大量使用了注解(decorator),进一步减少样板代码。
  • mobx:redux的替代方案,不同于redux的纯函数(pure function)的设计,深受面向对象(oo)和命令式编程( imperative)的影响

“redux很好,但是有太多的样板代码了” 。
现在可以不用再吐槽了。

探索 JavaScript 中的依赖管理及循环依赖问题

我们通常会把项目中使用的第三方依赖写在 package.json 文件里,然后使用 npm 、cnpm 或者 yarn 这些流行的依赖管理工具来帮我们管理这些依赖。但是它们是如何管理这些依赖的、它们之间有什么区别,如果出现了循环依赖应该怎么解决。

在回答上面几个问题之前,先让我们了解下语义化版本规则。

语义化版本

使用第三方依赖时,通常需要指定依赖的版本范围,比如

"dependencies": {
    "antd": "3.1.2",
    "react": "~16.0.1",
    "redux": "^3.7.2",
    "lodash": "*"
  }

上面的 package.json 文件表明,项目中使用的 antd 的版本号是 3.1.2,但是 3.1.1 和 3.1.2、3.0.1、2.1.1 之间有什么不同呢。语义化版本规则规定,版本格式为:主版本号.次版本号.修订号,并且版本号的递增规则如下:

  • 主版本号:当你做了不兼容的 API 修改
  • 次版本号:当你做了向下兼容的功能性新增
  • 修订号:当你做了向下兼容的问题修正

主版本号的更新通常意味着大的修改和更新,升级主版本后可能会使你的程序报错,因此升级主版本号需谨慎,但是这往往也会带来更好的性能和体验。次版本号的更新则通常意味着新增了某些特性,比如 antd 的版本从 3.1.1 升级到 3.1.2,之前的 Select 组件不支持搜索功能,升级之后支持了搜索。修订号的更新则往往意味着进行了一些 bug 修复。因此次版本号和修订号应该保持更新,这样能让你之前的代码不会报错还能获取到最新的功能特性。

但是,往往我们不会指定依赖的具体版本,而是指定版本范围,比如上面的 package.json 文件里的 react、redux 以及 lodash,这三个依赖分别使用了三个符号来表明依赖的版本范围。语义化版本范围规定:

  • ~:只升级修订号
  • ^:升级次版本号和修订号
  • *:升级到最新版本

因此,上面的 package.json 文件安装的依赖版本范围如下:

语义化版本规则定义了一种理想的版本号更新规则,希望所有的依赖更新都能遵循这个规则,但是往往会有许多依赖不是严格遵循这些规定的。因此,如何管理好这些依赖,尤其是这些依赖的版本就显得尤为重要,否则一不小心就会陷入因依赖版本不一致导致的各种问题中。

依赖管理

在项目开发中,通常会使用 npmyarn 或者 cnpm 来管理项目中的依赖,下面我们就来看看它们是如何帮助我们管理这些依赖的。

npm

npm 发展到今天,可以说经历过三个重大的版本变化。

npm v1

最早的 npm 版本在管理依赖时使用了一种很简单的方式。我们称之为嵌套模式。比如,在你的项目中有如下的依赖。

"dependencies": {
    A: "1.0.0",
    C: "1.0.0",
    D: "1.0.0"
}

这些模块都依赖 B 模块,而且依赖的 B模块的版本还不同。

通过执行 npm install 命令,npm v1 生成的 node_modules目录如下:

node_modules
├── [email protected]
│   └── node_modules
│       └── [email protected]
├── [email protected]
│   └── node_modules
│       └── [email protected]
└── [email protected]
    └── node_modules
        └── [email protected]

很明显,每个模块下面都会有一个 node_modules 目录存放该模块的直接依赖。模块的依赖下面还会存在一个 node_modules 目录来存放模块的依赖的依赖。很明显这种依赖管理简单明了,但存在很大的问题,除了 node_modules 目录长度的嵌套过深之外,还会造成相同的依赖存储多份的问题,比如上面的 [email protected] 就存放了两份,这明显也是一种浪费。于是在 npm v3 发布后,npm 的依赖管理做出了重大的改变。

npm v3

对于同样的上述依赖,使用 npm v3 执行 npm install 命令后生成的 node_modules 目录如下:

node_modules
├── [email protected]
├── [email protected]
└── [email protected]
    └── node_modules
        └── [email protected]
├── [email protected]

显而易见,npm v3 使用了一种扁平的模式,把项目中使用的所有的模块和模块的依赖都放在了 node_modules 目录下的顶层,遇到版本冲突的时候才会在模块下的 node_modules 目录下存放该模块需要用到的依赖。之所以能这么实现是基于包搜索机制的。包搜索机制是指当你在项目中直接 require('A') 时,首先会在当前路径下搜索 node_modules 目录中是否存在该依赖,如果不存在则往上查找也就是继续查找该路径的上一层目录下的 node_modules。正因为此,npm v3 才能把之前的嵌套结构拍平,把所有的依赖都放在项目根目录的 node_modules,这样就避免了 node_modules 目录嵌套过深的问题。此外,npm v3 还会解析模块的依赖的多个版本为一个版本,比如 A依赖 B@^1.0.1,D 依赖 B@^1.0.2,则只会有一个 [email protected] 的版本存在。虽然 npm v3 解决了这两个问题,但是此时的 npm 仍然存在诸多问题,被人诟病最多的应该就是它的不确定性了。

npm v5

什么是确定性。在 JavaScript 包管理的背景下,确定性是指在给定的 package.json 和 lock 文件下始终能得到一致的 node_modules 目录结构。简单点说就是无论在何种环境下执行 npm install 都能得到相同的 node_modules 目录结构。npm v5 正是为解决这个问题而产生的,npm v5 生成的 node_modules 目录和 v3 是一致的,区别是 v5 会默认生成一个 package-lock.json 文件,来保证安装的依赖的确定性。比如,对于如下的一个 package.json 文件

"dependencies": {
    "redux": "^3.7.2"
  }

对应的 package-lock.json 文件内容如下:

{
  "name": "test",
  "version": "1.0.0",
  "lockfileVersion": 1,
  "requires": true,
  "dependencies": {
    "js-tokens": {
      "version": "3.0.2",
      "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz",
      "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls="
    },
    "lodash": {
      "version": "4.17.4",
      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz",
      "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4="
    },
    "lodash-es": {
      "version": "4.17.4",
      "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.4.tgz",
      "integrity": "sha1-3MHXVS4VCgZABzupyzHXDwMpUOc="
    },
    "loose-envify": {
      "version": "1.3.1",
      "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.3.1.tgz",
      "integrity": "sha1-0aitM/qc4OcT1l/dCsi3SNR4yEg=",
      "requires": {
        "js-tokens": "3.0.2"
      }
    },
    "redux": {
      "version": "3.7.2",
      "resolved": "https://registry.npmjs.org/redux/-/redux-3.7.2.tgz",
      "integrity": "sha512-pNqnf9q1hI5HHZRBkj3bAngGZW/JMCmexDlOxw4XagXY2o1327nHH54LoTjiPJ0gizoqPDRqWyX/00g0hD6w+A==",
      "requires": {
        "lodash": "4.17.4",
        "lodash-es": "4.17.4",
        "loose-envify": "1.3.1",
        "symbol-observable": "1.1.0"
      }
    },
    "symbol-observable": {
      "version": "1.1.0",
      "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.1.0.tgz",
      "integrity": "sha512-dQoid9tqQ+uotGhuTKEY11X4xhyYePVnqGSoSm3OGKh2E8LZ6RPULp1uXTctk33IeERlrRJYoVSBglsL05F5Uw=="
    }
  }
}

不难看出,package-lock.json 文件里记录了安装的每一个依赖的确定版本,这样在下次安装时就能通过这个文件来安装一样的依赖了。

image

yarn

yarn 是在 2016.10.11 开源的,yarn 的出现是为了解决 npm v3 中的存在的一些问题,那时 npm v5 还没发布。yarn 被定义为快速、安全、可靠的依赖管理。

  • 快速:全局缓存、并行下载、离线模式
  • 安全:安装包被执行前校验其完整性
  • 可靠:lockfile文件、确定性算法

yarn 生成的 node_modules 目录结构和 npm v5 是相同的,同时默认生成一个 yarn.lock 文件。对于上面的例子,只安装 redux 的依赖生成的 yarn.lock 文件内容如下:

# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1

js-tokens@^3.0.0:
  version "3.0.2"
  resolved "http://registry.npm.alibaba-inc.com/js-tokens/download/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b"

lodash-es@^4.2.1:
  version "4.17.4"
  resolved "http://registry.npm.alibaba-inc.com/lodash-es/download/lodash-es-4.17.4.tgz#dcc1d7552e150a0640073ba9cb31d70f032950e7"

lodash@^4.2.1:
  version "4.17.4"
  resolved "http://registry.npm.alibaba-inc.com/lodash/download/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae"

loose-envify@^1.1.0:
  version "1.3.1"
  resolved "http://registry.npm.alibaba-inc.com/loose-envify/download/loose-envify-1.3.1.tgz#d1a8ad33fa9ce0e713d65fdd0ac8b748d478c848"
  dependencies:
    js-tokens "^3.0.0"

redux@^3.7.2:
  version "3.7.2"
  resolved "http://registry.npm.alibaba-inc.com/redux/download/redux-3.7.2.tgz#06b73123215901d25d065be342eb026bc1c8537b"
  dependencies:
    lodash "^4.2.1"
    lodash-es "^4.2.1"
    loose-envify "^1.1.0"
    symbol-observable "^1.0.3"

symbol-observable@^1.0.3:
  version "1.1.0"
  resolved "http://registry.npm.alibaba-inc.com/symbol-observable/download/symbol-observable-1.1.0.tgz#5c68fd8d54115d9dfb72a84720549222e8db9b32"

不难看出,yarn.lock 文件和 npm v5 生成的 package-lock.json 文件有如下几点不同:

  1. 文件格式不同,npm v5 使用的是 json 格式,yarn 使用的是一种自定义格式
  2. package-lock.json 文件里记录的依赖的版本都是确定的,不会出现语义化版本范围符号(~ ^ *),而 yarn.lock 文件里仍然会出现语义化版本范围符号
  3. package-lock.json 文件内容更丰富,npm v5 只需要 package.lock 文件就可以确定 node_modules 目录结构,而 yarn 却需要同时依赖 package.json 和 yarn.lock 两个文件才能确定 node_modules 目录结构

关于为什么会有这些不同、yarn 的确定性算法以及和 npm v5 的区别,yarn 官方的一篇文章详细介绍了这几点。由于篇幅有限,这里就不再赘述,感兴趣的可以移步到我的翻译文章 Yarn 确定性去看。

yarn 的出现除了带来安装速度的提升以外,最大的贡献是通过 lock 文件来保证安装依赖的确定性,保证相同的 package.json 文件,在何种环境何种机器上安装依赖都会得到相同的结果也就是相同的 node_modules 目录结构。这在很大程度上避免了一些“在我电脑上是正常的,在其他机器上失败”的 bug。但是在使用 yarn 做依赖管理时,仍然需要注意以下3点。

  • 不要手动修改 yarn.lock 文件
  • yarn.lock 文件应该提交到版本控制的仓库里
  • 升级依赖时,使用yarn upgrade命令,避免手动修改 package.json 和 yarn.lock 文件。

cnpm

cnpm 在国内的用户应该还是蛮多的,尤其是对于有搭建私有仓库需求的人来说。cnpm 在安装依赖时使用的是 npminstall,简单来说, cnpm 使用链接 link 的安装方式,最大限度地提高了安装速度,生成的 node_modules 目录采用的是和 npm 不一样的布局。 用 cnpm 装的包都是在 node_modules 文件夹下以 版本号 @包名 命名,然后再做软链接到只以包名命名的文件夹上。同样的例子,使用 cnpm 只安装 redux 依赖时生成的 node_modules 目录结构如下:

image

cnpm 和 npm 以及 yarn 之间最大的区别就在于生成的 node_modules 目录结构不同,这在某些场景下可能会引发一些问题。此外也不会生成 lock 文件,这就导致在安装确定性方面会比 npm 和 yarn 稍逊一筹。但是 cnpm 使用的 link 安装方式还是很好的,既节省了磁盘空间,也保持了 node_modules 的目录结构清晰,可以说是在嵌套模式和扁平模式之间找到了一个平衡。

npm、yarn 和 cnpm 均提供了很好的依赖管理来帮助我们管理项目中使用到的各种依赖以及版本,但是如果依赖出现了循环调用也就是循环依赖应该怎么解决呢?

循环依赖

循环依赖指的是,a 模块的执行依赖 b 模块,而 b 模块的执行又依赖 a 模块。循环依赖可能导致递归加载,处理不好的话可能使得程序无法执行。探讨循环依赖之前,先让我们了解一下 JavaScript 中的模块规范。因为,不同的规范在处理循环依赖时的做法是不同的。

目前,通行的 JavaScript 规范可以分为三种,CommonJSAMDES6

模块规范

CommonJS

从2009年 node.js 出现以来,CommonJS 模块系统逐渐深入人心。CommonJS 的一个模块就是一个脚本文件,通过 require 命令来加载这个模块,并使用模块暴漏出的接口。加载时执行是 CommonJS 模块的重要特性,即脚本代码在 require 的时候就会执行模块中的代码。这个特性在服务端是没问题的,但如果引入一个模块就要等待它执行完才能执行后面的代码,这在浏览器端就会有很大的问题了。因此出现了 AMD 规范,以支持浏览器环境。

AMD

AMD 是 “Asynchronous Module Definition” 的缩写,意思就是“异步模块定义”。它采用异步加载方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。最有代表性的实现则是 requirejs

ES6

不同于 CommonJS 和 AMD 的模块加载方案,ES6 在 JavaScript 语言层面上实现了模块功能。它的设计**是,尽量的静态化,使得编译时就能确定模块的依赖关系。在遇到模块加载命令 import 时,不会去执行模块,而是只生成一个引用。等到真的需要用到时,再到模块里面去取值。这是和 CommonJS 模块规范的最大不同。

CommonJS 中循环依赖的解法

请看下面的例子:

a.js

console.log('a starting');
exports.done = false;
const b = require('./b.js');
console.log('in a, b.done = %j', b.done);
exports.done = true;
console.log('a done');

b.js

console.log('b starting');
exports.done = false;
const a = require('./a.js');
console.log('in b, a.done = %j', a.done);
exports.done = true;
console.log('b done');

main.js

console.log('main starting');
const a = require('./a.js');
const b = require('./b.js');
console.log('in main, a.done=%j, b.done=%j', a.done, b.done);

在这个例子中,a 模块调用 b 模块,b 模块又需要调用 a 模块,这就使得 a 和 b 之间形成了循环依赖,但是当我们执行 node main.js 时代码却没有陷入无限循环调用当中,而是输出了如下内容:

$ node main.js
main starting
a starting
b starting
in b, a.done = false
b done
in a, b.done = true
a done
in main, a.done=true, b.done=true

为什么程序没有报错,而是输出如上的内容呢?这是因为 CommonJs 模块的两个特性。第一,加载时执行;第二,已加载的模块会进行缓存,不会重复加载。下面让我们分析下程序的执行过程:

  1. main.js 执行,输出 main starting
  2. main.js 加载 a.js,执行 a.js 并输出 a starting,导出 done = false
  3. a.js 加载 b.js,执行 b.js 并输出 b starting,导出 done = false
  4. b.js 加载 a.js,由于之前 a.js 已加载过一次因此不会重复加载,缓存中 a.js 导出的 done = false,因此,b.js 输出 in b, a.done = false
  5. b.js 导出 done = true,并输出 b done
  6. b.js 执行完毕,执行权交回给 a.js,执行 a.js,并输出 in a, b.done = true
  7. a.js 导出 done = true,并输出 a done
  8. a.js 执行完毕,执行权交回给 main.js,main.js 加载 b.js,由于之前 b.js 已加载过一次,不会重复执行
  9. main.js 输出 in main, a.done=true, b.done=true

从上面的执行过程中,我们可以看到,在 CommonJS 规范中,当遇到 require() 语句时,会执行 require 模块中的代码,并缓存执行的结果,当下次再次加载时不会重复执行,而是直接取缓存的结果。正因为此,出现循环依赖时才不会出现无限循环调用的情况。虽然这种模块加载机制可以避免出现循环依赖时报错的情况,但稍不注意就很可能使得代码并不是像我们想象的那样去执行。因此在写代码时还是需要仔细的规划,以保证循环模块的依赖能正确工作(官方原文:Careful planning is required to allow cyclic module dependencies to work correctly within an application)。

除了仔细的规划还有什么办法可以避免出现循环依赖吗?一个不太优雅的方法是在循环依赖的每个模块中先写 exports 语句,再写 require 语句,利用 CommonJS 的缓存机制,在 require() 其他模块之前先把自身要导出的内容导出,这样就能保证其他模块在使用时可以取到正确的值。比如:

A.js

exports.done = true;

let B = require('./B');
console.log(B.done)

B.js

exports.done = true;

let A = require('./A');
console.log(A.done)

这种写法简单明了,缺点是要改变每个模块的写法,而且大部分同学都习惯了在文件开头先写 require 语句。

个人经验来看,在写代码中只要我们注意一下循环依赖的问题就可以了,大部分同学在写 node.js 中应该很少碰到需要手动去处理循环依赖的问题,更甚的是很可能大部分同学都没想过这个问题。

ES6 中循环依赖的解法

要想知道 ES6 中循环依赖的解法就必须先了解 ES6 的模块加载机制。我们都知道 ES6 使用 export 命令来规定模块的对外接口,使用 import 命令来加载模块。那么在遇到 import 和 export 时发生了什么呢?ES6 的模块加载机制可以概括为四个字一静一动

  • 一静:import 静态执行
  • 一动:export 动态绑定

import 静态执行是指,import 命令会被 JavaScript 引擎静态分析,优先于模块内的其他内容执行。
export 动态绑定是指,export 命令输出的接口,与其对应的值是动态绑定关系,通过该接口可以实时取到模块内部的值。

让我们看下面一个例子:

foo.js

console.log('foo is running');
import {bar} from './bar'
console.log('bar = %j', bar);
setTimeout(() => console.log('bar = %j after 500 ms', bar), 500);
console.log('foo is finished');

bar.js

console.log('bar is running');
export let bar = false;
setTimeout(() => bar = true, 500);
console.log('bar is finished');

执行 node foo.js 时会输出如下内容:

bar is running
bar is finished
foo is running
bar = false
foo is finished
bar = true after 500 ms

是不是和你想的不一样呢?当我们执行 node foo.js 时第一行输出的不是 foo.js 的第一个 console 语句,而是先输出了 bar.js 里的 console 语句。这就是因为 import 命令是在编译阶段执行,在代码运行之前先被 JavaScript 引擎静态分析,所以优先于 foo.js 自身内容执行。同时我们也看到 500 毫秒之后也可以取到 bar 更新后的值也说明了 export 命令输出的接口与其对应的值是动态绑定关系。这样的设计使得程序在编译时就能确定模块的依赖关系,这是和 CommonJS 模块规范的最大不同。还有一点需要注意的是,由于 import 是静态执行,所以 import 具有提升效果即 import 命令的位置并不影响程序的输出。

在我们了解了 ES6 的模块加载机制之后来让我们来看一下 ES6 是怎么处理循环依赖的。修改一下上面的例子:

foo.js

console.log('foo is running');
import {bar} from './bar'
console.log('bar = %j', bar);
setTimeout(() => console.log('bar = %j after 500 ms', bar), 500);
export let foo = false;
console.log('foo is finished');

bar.js

console.log('bar is running');
import {foo} from './foo';
console.log('foo = %j', foo)
export let bar = false;
setTimeout(() => bar = true, 500);
console.log('bar is finished');

执行 node foo.js 时会输出如下内容:

bar is running
foo = undefined
bar is finished
foo is running
bar = false
foo is finished
bar = true after 500 ms

foo.js 和 bar.js 形成了循环依赖,但是程序却没有因陷入循环调用报错而是执行正常,这是为什么呢?还是因为 import 是在编译阶段执行的,这样就使得程序在编译时就能确定模块的依赖关系,一旦发现循环依赖,ES6 本身就不会再去执行依赖的那个模块了,所以程序可以正常结束。这也说明了 ES6 本身就支持循环依赖,保证程序不会因为循环依赖陷入无限调用。虽然如此,但是我们仍然要尽量避免程序中出现循环依赖,因为可能会发生一些让你迷惑的情况。注意到上面的输出,在 bar.js 中输出的 foo = undefined,如果没注意到循环依赖会让你觉得明明在 foo.js 中 export foo = false,为什么在 bar.js 中却是 undefined 呢,这就是循环依赖带来的困惑。在一些复杂大型项目中,你是很难用肉眼发现循环依赖的,而这会给排查异常带来极大的困难。对于使用 webpack 进行项目构建的项目,推荐使用 webpack 插件 circular-dependency-plugin 来帮助你检测项目中存在的所有循环依赖,尽早发现潜在的循环依赖可能会免去未来很大的麻烦。

小结

讲了那么多,希望此文能帮助你更好的了解 JavaScript 中的依赖管理,并且处理好项目中的循环依赖问题。

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.