译者:poppinlp
你可以在完全无风险的情况下更新 Node 依赖
在 left-pad 风波平息之后,我们应该回头想想如何更好的使用 node 模块,确切的说是如何在安全、可靠且可重构(至少 Javascript 能够允许的程度)的前提下更新 node 包。
问题
最近,我被要求更新我们过时的 react-router 到新版本。在我书写这篇文章的时候最新版本应该是 2.4.2,而我们使用的还是 0.13.3。
在我们的应用中,路由部分可能是前端使用的最难理解和麻烦的代码。它非常依赖一个家庭网历史模块,可容纳传统的路由并重定向到正确的路径位置,并采用一些“智能路由”以根据它的ID和上下文来决定对象的路线。我们也在许多贯穿应用的地方暴露出了路由器对象,以方便编程路由的目标(这些东西是在更新的 react-router 版本中不支持的)。
解决方法
我们最近请来了两个重构的专家,提供关于正确的重构以前代码的一些指导,并密切关注 Martin Fowler 的 pivotal book on the subject 一书。不幸的是,专家的指导没有像前端工程师们所喜欢的那样深入的挖掘重构前端代码,但从指导中我们仍旧收获了一些可以用于工作中的有价值的东西。我会利用我们从指导中了解的工具来更新这个模块。
首要的事情
幸好 react-router 有大量关于 主版本 升级的文档。我们通过研究文档了解到了我们需要做出的重大的修改。
除了涉及到迁移到新模块的过程外,我不会深入聊到升级的细节。
功能标志
Javascript 真的不太安全。几乎你在代码中做的任何事情都可能引入某种副作用,甚至只是很平常的引入一个模块(事实上,关于这一点,我发现在一个新模块导入后,再导入某个修改了全局状态的模块,将会导致新模块出问题)。
安全地修改代码的最好方法是引入的功能标志或者环境变量又或者一些其他的环境因子,通过这些来控制引入的新的代码。在我们的 Java 应用中,我们使用一个叫做 Togglz 的插件将值存储在数据库中,并将它们注入到前端。
我们也使用 Webpack。我们可以放弃 Java 的实现并且可以在 Webpack 配置信息中提供我们想要的配置。这些可以通过一个别名模块来实现:
// In the webpack config
...
resolve: {
alias: {
featureFlags: {
'react_013_to_1': true
}
}
}
...
// In a file that needs the feature flag
var featureFlags = require('featureFlags');
if (featureFlags.react_013_to_1) {
// do something
}
复制代码
有很多方法可以实现这个,找到最适合你的环境和构建流程的方法即可。
同时存在两个不同版本的模块?
现在我们有办法可以开启和关闭之前说到的新功能了,下来就是寻找一个可以安全的可以同时导入一个模块的两个不同版本的方法。事实证明,如果我们打破一些规则,那么这是非常容易实现的。
我们现在使用着 0.13.3 版本的 react-router,第一步我们希望能升级到 1.0.3 或者 1.x.x 中可用的更高版本。原因在于我们希望能尽可能的接近 2.x.x 版本以便能够更轻松的进行升级过渡。
由于 react-router 是我们的前端入口的直接依赖,所以其实没有真正的方法可以绕过同时只能运行依赖模块某一个版本的事实 — 至少在 npm 中无法绕过 。我们_能做_的只是从 github 上克隆 react-router 到本地并构建好,然后把它的库添加到我们的应用中使其进入我们的代码库。
这时,你可能会觉得有问题:在任何情况下我们都不应该把一个外部库放进我们自己的代码中,并且 node_modules 应该是你 .gitignore 文件中的一部分。但这个规则的目的是使得我们的构建流程可转移并且避免代码库附带有非常多的依赖库代码。但我们目前的情况,是需要做一些临时并且和构建无关的事情。
过程是这样的:
- 克隆 react-router 项目
-
进入克隆下来的项目,通过
git checkout
切换到 1.0.3 的版本 -
通过
npm install
来安装依赖 -
通过
npm run build
来构建 1.0.3 版本的结果 - 在前端代码中创建 module-upgrades 目录,并在里面创建 react-router-1 目录
- 复制 react-router 模块生成的库目录到 react-router-1 目录中
现在我们便可以通过切换功能标志开关来导入和使用两个不同版本的 react-router 了。当我们完全测试完新版后,便来到了这个方案的最后一步,也就是更新应用的 package.json 文件,使用
^1.0.0
版本的 react-router,然后将对于我们代码中加入的 react-router (也就是 ""react-router-1"")的引用重构成对于真正 node_modules 中的模块的引用(定义在 package.json 中的模块),然后继续愉快的开发。
组件迁移
在这个路由的例子中,我们希望能每次更改一个路由并测试它。首先,我们有这么一个测试方法:
- 直接触发路由
- 通过前进按钮触发路由
- 通过后退按钮触发路由
- 通过以前的路由来触发路由(如果可以的话)
- 通过程序接口来触发路由
测试方法多种多样,你也可以实现自己的方法。由于我们这部分的单元测试参差不齐,所以我们决定每次更改路由后至少应该做一个冒烟测试。
接下来,我们创建了一个切换路由的地方 。我们所有的路由都在一个名为 Routes.jsx 的文件中,其中路由仅仅只是连接到一个数组中,然后将其导出以备后用。那个文件看起来像是这样:
var Route = require('react-router').Route;
var routes = [
,
,
...
];
module.export = routes;
复制代码
由于这个文件是给旧路由使用的,所以我们最好以 Routes.jsx 为副本,创建一个叫做 NewRoutes.jsx 的临时的新文件,用于逐步的迁移组件。当我们准备好移除功能标示时,我们只需要做一点哪怕是在 Javascript 中也非常容易的重构工作,即将旧的 Routes.jsx 删除并把 NewRoutes.jsx 重命名。
NewRoutes.jsx 类似于这样:
var Route = require('react-router').Route,
NewRoute = require('module-upgrades/react-router');
var routes = [
, // migrated
, // to be migrated
...
];
module.export = routes;
复制代码
在迁移各个路由的过程中,我们需要同时加载两个路由器。等到一旦迁移完了所有路由,我们便可以删除对于旧路由的引用,并且 NewRouter.jsx 将只会处理新路由。
抽象掉差别
现在,我们已经知道了如何安全的测试修改并且重构依赖关系,接下来我们就开始迁移我们的代码。
因为这部分的细节很大程度上取决于具体的迁移,所以探究我们基于 react-router 做的一些具体的改变意义不大 — 唯一有用的就是我们想 创建 一组能够跨代码库应用的的修改。
在我们的应用中,本质上我们有两种不同类型的路由逻辑:
- 我们通过名字找到目标(react-router 0.13 的遗留物)
- 我们通过名字找到目标,并提供数据
因为方案 2 只是方案 1 的扩展,所以我们可以通过扩展方案 1 的方式来让它与方案 2 一起工作。
这是我们的一个旧路由逻辑的例子:
...
var RouterContainer = require('./../../routes/RouterContainer'),
...
RouterContainer.get().transitionTo(DocumentTypeToPathMap[documentType], { id: this.props.id }, { projectId: this.getProjectId() });
...
复制代码
我们拿到了一个包含路由器单例(通过静态的 ""get"" 方法获取)的 ""RouterContainer"" 的引用。然后我们通过传递路由名字、参数(这个例子中只是一个 id)以及查询的值(projectId),来调用它的 ""transitionTo"" 方法。
由于 react-router 新版的历史处理由新的 history 这个依赖模块所代理,所以不再有能传递参数给路由的内置的方法,于是我们需要创建一个新的方法用于接收一个路由和一个 id,并把他们结合成一个预期结构的路由。然而,新的 history 依赖模块允许我们传递一个查询字符串。
现在我们有一个适用于所有路由逻辑的更改方式:
上面的例子如果想在新的场景下工作,必须被转换成这样:
...
var NewJamaLocation = require('jama/routes/NewJamaLocation'),
...
NewJamaLocation.push({ pathname: NewDocumentTypeToPathMap.documentTypeToPath(documentType, this.props.id), search: '?projectId='+this.getProjectId() });
复制代码
NewJamaLocation 表示新的 history 依赖模块的单例,它有一个类似于 transitionTo.push 的 push 方法接受一个包含 pathname 和 search 值的对象。我们已经把路径的公式抽象成了 NewDocumentTypetoPathMap 对象,这个对象包含一个静态方法可以接受 documentType 和 id 作为参数并创建一个可用的路径。