Обычно мы записываем сторонние зависимости, используемые в проекте, в файл 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 не поддерживает функцию поиска, но после обновления поддерживается функция поиска. Обновление номера версии обычно означает исправление некоторых ошибок. Таким образом, дополнительный номер версии и номер редакции следует поддерживать в актуальном состоянии, чтобы вы могли получать последние функции, не сообщая об ошибках в предыдущем коде.
Однако часто мы не указываем конкретную версию зависимости, а указываем диапазон версий, таких как react, redux и lodash, в файле package.json выше, Эти три зависимости используют три символа для обозначения диапазона версий зависимости. Спецификации области семантического управления версиями:
- ~: обновлять только номера ревизий
- ^: Обновить дополнительный номер версии и номер редакции.
- *: Обновите до последней версии
Таким образом, диапазоны версий зависимостей, установленных вышеуказанным файлом package.json, следующие:
- реагировать@~16.0.1: >=реагировать@16.0.1 &&
- redux@^3.7.2: >=redux@3.7.2 &&
- lodash@*:lodash@последний
Правила семантического управления версиями определяют идеальное правило обновления номера версии. Есть надежда, что все обновления зависимостей могут следовать этому правилу, но часто бывает много зависимостей, которые строго не следуют этим правилам. Поэтому то, как управлять этими зависимостями, особенно версиями этих зависимостей, особенно важно, иначе вы столкнетесь с различными проблемами, вызванными несогласованными версиями зависимостей.
управление зависимостями
В разработке проектов обычно используетсяnpm,yarnилиcnpmЧтобы управлять зависимостями в проекте, давайте посмотрим, как они помогают нам управлять этими зависимостями.
npm
npmНа данный момент можно сказать, что он претерпел три основных изменения версии.
npm v1
Самые ранние версии npm использовали очень простой способ управления зависимостями. Мы называем это вложенным шаблоном. Например, в вашем проекте у вас есть следующие зависимости.
"dependencies": {
A: "1.0.0",
C: "1.0.0",
D: "1.0.0"
}
Все эти модули зависят от модуля B, и версия зависимого модуля B отличается.
A@1.0.0 -> B@1.0.0
C@1.0.1 -> B@2.0.0
D@1.0.0 -> B@1.0.0
выполнивnpm install
Команда, каталог node_modules, сгенерированный npm v1, выглядит следующим образом:
node_modules
├── A@1.0.0
│ └── node_modules
│ └── B@1.0.0
├── C@1.0.0
│ └── node_modules
│ └── B@2.0.0
└── D@1.0.0
└── node_modules
└── B@1.0.0
Очевидно, что под каждым модулем будет каталог node_modules для хранения прямых зависимостей модуля. Также будет каталог node_modules под зависимостями модуля для хранения зависимостей зависимостей модуля. Очевидно, что такой вид управления зависимостями прост и понятен, но есть большая проблема.Помимо глубокой вложенности длины каталога node_modules, это также вызовет проблему множественных копий одного и того же хранилища зависимостей.Например, B@1.0.0 выше хранится Это явно пустая трата денег на двоих. Итак, после выпуска npm v3 в управление зависимостями npm были внесены серьезные изменения.
npm v3
Для тех же вышеперечисленных зависимостей используйте npm v3 для выполненияnpm install
Каталог node_modules, сгенерированный после команды, выглядит следующим образом:
node_modules
├── A@1.0.0
├── B@1.0.0
└── C@1.0.0
└── node_modules
└── B@2.0.0
├── D@1.0.0
Очевидно, что 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, будет только одна версия B. @1.0.2. . Хотя npm v3 решает эти две проблемы, в настоящее время с npm все еще остается много проблем, наиболее критикуемой из которых должна быть его неопределенность.
npm v5
Что такое определенность. В контексте управления пакетами JavaScript детерминизм означает постоянное получение согласованной структуры каталогов node_modules с учетом файлов package.json и блокировки. Проще говоря, выполняется в любой средеnpm install
Оба получают одинаковую структуру каталогов node_modules. Для решения этой проблемы был создан npm v5.Каталог node_modules, сгенерированный npm v5, такой же, как и в 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 записывает определенную версию каждой установленной зависимости, так что эту же зависимость можно установить через этот файл при следующей установке.
yarn
yarnОн был открыт в 2016.10.11, и пряжа появилась для решения некоторых проблем в npm v3, когда npm v5 еще не был выпущен. пряжа определяется как быстрое, безопасное и надежное управление зависимостями.
- Быстро: глобальный кеш, параллельная загрузка, автономный режим
- Безопасность: целостность установочного пакета проверяется перед его выполнением.
- Надежность: файлы блокировки, детерминированные алгоритмы
Структура каталогов node_modules, созданная yarn, такая же, как и в npm v5, а файл yarn.lock создается по умолчанию. В приведенном выше примере содержимое файла yarn.lock, созданного путем установки только зависимостей redux, выглядит следующим образом:
# 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 отличается от файла package-lock.json, сгенерированного npm v5, в следующих моментах:
- Формат файла другой, npm v5 использует формат json, пряжа использует пользовательский формат
- Все версии зависимостей, записанные в файле package-lock.json, являются детерминированными, и символы диапазона semver (~ ^ *) не будут отображаться, в то время как символы диапазона semver по-прежнему будут отображаться в файле yarn.lock.
- Файл package-lock.json богаче по содержанию: npm v5 нужен только файл package.lock для определения структуры каталогов node_modules, в то время как yarn должен полагаться на файлы package.json и yarn.lock для определения структуры каталогов node_modules. .
Относительно того, почему существуют эти различия, детерминированный алгоритм пряжи и отличие от npm v5, официальная статья о пряже подробно описывает эти моменты. Из-за ограниченного места я не буду здесь вдаваться в подробности, если вам интересно, вы можете перейти к моей статье о переводе.Пряжа Уверенностьиди и смотри.
yarnВ дополнение к улучшению скорости установки, самый большой вклад состоит в том, чтобы обеспечить уверенность в зависимости от установочных зависимостей через файл блокировки, чтобы обеспечить тот же пакет. Это те же структура каталогов Node_Modules. Это в значительной степени избегает некоторых «Что работает на моем компьютере, не удается на других» ошибках. Но при использовании пряжи для управления зависимостями вам все равно нужно обратить внимание на следующие 3 балла.
- Не изменяйте вручную файл yarn.lock
- Файл yarn.lock должен быть помещен в репозиторий с контролем версий.
- При обновлении зависимостей используйте
yarn upgrade
чтобы избежать ручного изменения файлов package.json и yarn.lock.
cnpm
cnpmПользователей в Китае должно быть довольно много, особенно тех, кому нужно построить частный склад. cnpm использует следующее при установке зависимостейnpminstallПроще говоря, cnpm использует метод установки по ссылке, чтобы максимизировать скорость установки, а сгенерированный каталог node_modules использует другой макет, чем npm. Пакеты, установленные с помощью cnpm, именуются в папке node_modules с номером версии @package name, а затем мягко связываются с папкой, названной только именем пакета. В том же примере структура каталогов node_modules, созданная при использовании cnpm для установки только избыточных зависимостей, выглядит следующим образом:
Самая большая разница между cnpm и npm и yarn заключается в том, что сгенерированная структура каталогов node_modules отличается, что может вызвать некоторые проблемы в некоторых сценариях. Кроме того, файлы блокировки не генерируются, что делает его немного менее надежным, чем npm и yarn с точки зрения надежности установки. Тем не менее, метод установки ссылок, используемый cnpm, по-прежнему очень хорош, что не только экономит место на диске, но и сохраняет четкость структуры каталогов node_modules.Можно сказать, что он нашел баланс между вложенным режимом и плоским режимом.
npm, yarn и cnpm обеспечивают хорошее управление зависимостями, помогая нам управлять различными зависимостями и версиями, используемыми в проекте, но если в зависимости есть циклический вызов, то как решить циклическую зависимость?
круговая зависимость
Циклическая зависимость означает, что выполнение модуля а зависит от модуля b, который, в свою очередь, зависит от модуля а. Циклические зависимости могут привести к рекурсивной загрузке, что при неправильном обращении может сделать выполнение программы невозможным. Прежде чем мы перейдем к циклическим зависимостям, давайте взглянем на спецификацию модуля в JavaScript. Потому что разные спецификации по-разному относятся к циклическим зависимостям.
В настоящее время общие спецификации JavaScript можно разделить на три типа:CommonJS,AMDа такжеES6.
Спецификация модуля
CommonJS
С момента появления node.js в 2009 г.CommonJSМодульная система постепенно набирает популярность. Модуль CommonJS представляет собой файл сценария, черезrequire
команда для загрузки этого модуля и использования интерфейса, предоставляемого модулем. Выполнение во время загрузки — важная особенность модулей CommonJS, то есть код скриптаrequire
Когда код в модуле выполняется. Эта функция хороша на стороне сервера, но если модуль введен, он будет ждать завершения его выполнения, прежде чем выполнять следующий код, что будет большой проблемой на стороне браузера. Отсюда появилсяAMDспецификация для поддержки среды браузера.
AMD
AMDЭто аббревиатура от «Определение асинхронного модуля», что означает «определение асинхронного модуля». Он использует асинхронную загрузку для загрузки модулей, и загрузка модулей не влияет на выполнение следующих за ней операторов. Все операторы, которые зависят от этого модуля, определяются в функции обратного вызова, которая не будет выполняться до тех пор, пока загрузка не будет завершена. Наиболее репрезентативной реализацией является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. Во-первых, он выполняется при загрузке, во-вторых, загруженный модуль будет закэширован и не будет загружаться повторно. Проанализируем процесс выполнения программы:
- main.js выполняется, вывод основного запуска
- main.js загружает a.js, выполняет a.js и выводит start, export done = false
- a.js загружает b.js, выполняет b.js и выводит запуск b, экспорт выполнен = false
- b.js загружает a.js. Поскольку a.js был загружен один раз, он не будет загружаться повторно. Done = false экспортируется a.js в кеш, поэтому вывод b.js находится в b, a.done = ложь
- экспорт b.js выполнен = true, а вывод b выполнен
- После выполнения b.js право на выполнение возвращается к a.js, выполняется a.js и выводится в a, b.done = true
- a.js экспортирует done = true и выводит done
- После выполнения a.js право на выполнение возвращается main.js, а main.js загружает b.js, так как b.js уже был загружен один раз, он не будет выполняться повторно.
- вывод main.js в main, a.done=true, b.done=true
Из приведенного выше процесса выполнения мы видим, что в спецификации CommonJS при встречеrequire()
Когда оператор будет выполнен, код в модуле require будет выполнен, а результат выполнения будет закеширован.Когда он будет загружен снова в следующий раз, выполнение не будет повторяться, а будет непосредственно извлечен закешированный результат. Из-за этого нет бесконечного цикла вызовов, когда есть циклическая зависимость. Хотя этот механизм загрузки модуля может избежать ситуации, когда циклические зависимости сообщают об ошибках, небольшая небрежность может привести к тому, что код не будет выполняться так, как мы предполагали. Поэтому при написании кода требуется тщательное планирование, чтобы гарантировать правильную работу зависимостей циклических модулей (официальный исходный текст: требуется тщательное планирование, чтобы позволить зависимостям циклических модулей правильно работать в приложении).
Есть ли способ избежать циклических зависимостей, кроме тщательного планирования? Менее элегантный метод заключается в том, чтобы сначала написать оператор 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
Команда загрузки модуля. Так что же произошло тогда перед лицом импорта и экспорта? Механизм загрузки модулей ES6 можно описать двумя словами.будь спокоен.
- Один статический: импортировать статическое выполнение
- Один шаг: экспортировать динамическую привязку
Статическое выполнение импорта означает, что команда импорта будет статически проанализирована движком JavaScript и выполнена перед другим содержимым модуля.
Экспорт динамической привязки означает, что вывод интерфейса командой экспорта имеет связь динамической привязки с соответствующим значением, и значение внутри модуля может быть получено в режиме реального времени через этот интерфейс.
Давайте посмотрим на пример:
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, а консольная инструкция bar.js. Это связано с тем, что команда импорта выполняется на этапе компиляции и статически анализируется движком JavaScript перед запуском кода, поэтому она имеет приоритет над содержимым самого файла foo.js. В то же время мы также видим, что обновленное значение bar также может быть получено через 500 миллисекунд, что также показывает, что интерфейс, выводимый командой экспорта, и соответствующее ему значение динамически связаны. Этот дизайн позволяет программе определять зависимости модуля во время компиляции, что является самым большим отличием от спецификации модуля CommonJS. Еще один момент, который следует отметить, заключается в том, что, поскольку импорт выполняется статически, импорт имеет эффект подъема, то есть расположение команды импорта не влияет на вывод программы.
После того, как мы разберемся с механизмом загрузки модулей 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 образуют циклическую зависимость, но программа выполняется нормально, не попадая в циклический вызов и не сообщая об ошибке. Почему это так? Или потому, что импорт выполняется на этапе компиляции, чтобы программа могла определить зависимости модулей во время компиляции.Как только циклическая зависимость будет найдена, ES6 сам больше не будет выполнять зависимый модуль, поэтому программа может завершиться нормально. Это также показывает, что сам ES6 поддерживает циклические зависимости, гарантируя, что программа не попадет в бесконечные вызовы из-за циклических зависимостей. Тем не менее, мы по-прежнему стараемся избегать циклических зависимостей в наших программах, потому что могут возникать ситуации, которые могут вас смутить. Обратите внимание на вывод выше, вывод в bar.jsfoo = undefined
, если вы не заметили, что круговая зависимость заставит вас думать, что это явно в foo.jsexport foo = false
, почему это в bar.jsundefined
Ну, это путаница, вызванная циклическими зависимостями. В некоторых сложных и масштабных проектах невооруженным глазом сложно найти круговые зависимости, что принесет большие трудности при устранении неполадок. Для проектов, которые используют веб-пакет для сборки проекта, рекомендуется использовать плагин веб-пакета.circular-dependency-pluginЧтобы помочь вам обнаружить все циклические зависимости, существующие в вашем проекте, раннее обнаружение потенциальных циклических зависимостей может избавить вас от многих проблем в будущем.
резюме
Сказав так много, я надеюсь, что эта статья поможет вам лучше понять управление зависимостями в JavaScript и иметь дело с циклическими зависимостями в проектах.