本文转载自:众成翻译
译者:iOSDevLog
链接:http://www.zcfy.cc/article/3803
原文:https://www.fullstackreact.com/30-days-of-react/day-27/
今天,我们将探讨部署我们的应用所涉及的不同部分,以便外界可以使用我们的应用。
我们的应用通过这一点进行了测试, 现在是时候让它起来为世界而活。本课程的其余部分将致力于将我们的应用部署到生产中。
生产部署
在谈到部署时, 我们有很多不同的选择:
主机
部署环境配置
持续集成 (简称 CI)
成本周期、网络带宽成本
包大小
更多
我们将看看不同的托管选项, 明天看看部署我们的react应用的一些不同的方法, 我们部署我们的应用。今天, 我们将专注于让我们的应用准备好部署。
弹出 (从create-react-app
)
首先, 我们需要在 web 应用中处理一些自定义, 所以我们需要在目录的根中运行 npm run eject
命令。这是一个永久性的动作,现在这只是意味着我们将负责处理我们的应用结构的自定义 (没有我们的方便create-react-app
的帮助)。
这是我 总是 说, 做一个备份副本的应用。我们不能从
ejecting
返回, 但我们可以恢复到旧代码。
我们可以通过运行由create-react-app
结构生成器提供的弹出命令来 _弹出_:
npm run eject
在 ejecting 的create-react-app
结构中, 我们将看到我们的应用根目录中有很多新文件在config/
和 scripts/
目录。npm run eject
命令创建了它在内部使用的所有文件, 并在我们的应用中为我们编写了所有的文档。
create-react-app`生成器的关键方法称为webpack, 它是一个模块打包器/生成器。
webpack 基础知识
Webpack 是一个大社区的用户模块打包器, 成吨的插件正在积极开发, 有一个聪明的插件系统, 是令人难以置信的快速, 支持热代码重装, 和更多的多。
虽然我们没有真正调用它之前, 我们一直在使用 webpack 这整个时间 (在npm start
的幌子下)。如果没有 webpack, 我们就不可能只写import
, 并期望我们的代码加载。它的工作原理像这样, 因为 webpack “看到”import
的关键字, 并且知道我们需要在应用运行时可以访问路径上的代码。
Webpack 为我们照顾热加载, 几乎自动, 可以加载和打包许多类型的文件包, 它可以以逻辑方式拆分代码, 以便支持延迟加载和收缩用户的初始下载大小。
这对我们是有意义的, 因为我们的应用越来越大, 更复杂, 重要的是要知道如何操纵我们的构建工具。
例如, 当我们要部署到不同的环境..。首先, 对 webpack 的一个微小的介绍, 它是什么以及它是如何工作的。
bundle.js
做什么
当我们运行 npm start
查看生成的文件之前我们弹出的应用,我们可以看到它为浏览器服务两个或更多的文件。第一个是index.html
和bundle.js
.。webpack 服务器负责将bundle.js
.插入index.html
, 即使我们不在index.html
文件中加载我们的应用。
bundle.js
文件是一个巨大的文件, 包含我们的应用需要运行的 所有 的 JavaScript 代码, 包括依赖和我们自己的文件。Webpack 有它自己的方法包装文件在一起, 所以当看原始的源码它看起来有点有趣。
Webpack 已经对所有包含的 JavaScript 进行了一些转换。值得注意的是, 它使用Babel以 ES5-compatible 的格式转换我们的 ES6 代码, 。
如果您查看 app.js
,的注释头, 它有一个数字 “254”:
/* 254 */
/*!********************!*\
!*** ./src/app.js ***!
\********************/
模块本身封装在一个类似如下的函数中:
function(module, exports, __webpack_require__) {
// The chaotic `app.js` code here
}
我们的 web 应用的每个模块都用这个签名封装在一个函数里面。Webpack 已经给我们的每个应用的模块这个功能容器以及模块 ID (在app.js
的情况下, 254)。
但是这里的 “模块” 不限于 ES6 模块。
Remember how we “imported” the makeRoutes()
function in app.js
, like this:请记住, 我们是如何在app.js
“导入” makeRoutes()
函数的, 如下所示:
import makeRoutes from './routes'
这里的变量声明的makeRoutes
看起来像在混乱的app.js
Webpack 模块:
var _logo = __webpack_require__(/*! ./src/routes.js */ 255);
他看起来很奇怪, 主要是因为 Webpack 为调试目的提供的在线评论。删除该注释:
var _logo = __webpack_require__(255);
我们有简单的旧 ES5 代码, 而不是import
语句。
现在, 在这个文件中搜索./src/routes.js
。
/* 255 */
/*!**********************!*\
!*** ./src/routes.js ***!
\**********************/
请注意, 它的模块 ID 是 “255”, 相同的整数传递给上面的 __webpack_require__
。
Webpack 将 一切 视为一个模块, 包括像logo.svg
这样的图像资产。我们可以通过在logo.svg
模块的混乱中挑选出一条路径来了解发生了什么。您的路径可能不同, 但它看起来像这样:
static/media/logo.5d5d9eef.svg
如果您打开一个新的浏览器标签并插入这个地址 (您的地址将是不同的… 匹配为您生成的文件 webpack 的名称):
http://localhost:3000/static/media/logo.5d5d9eef.svg
你应该得到的React Logo:
因此, Webpack 为 logo.svg
创建了一个 Webpack 模块, 它指的是 Webpack 开发服务器上的 svg 路径。由于这种模块化范例, 它能够智能地编译如下语句:
import makeRoutes from './routes'
进入这 ES5 声明:
var _makeRoutes = __webpack_require__(255);
我们的 CSS 资产呢?是的, 一切 是 Webpack 的一个模块。搜索字符串./src/app.css
:
Webpack 的index.html
没有包含任何对 CSS 的引用。这是因为 Webpack 是通过bundle.js
包括我们的 CSS 在这里。当我们的应用加载时, 这个神秘的 Webpack 模块函数将app.css
的内容转储到页面上的style
标签中。
因此, 我们知道 _什么 正在发生: Webpack 已经卷起每一个可以想象的 “模块” 为我们的应用进入bundle.js
‘。你可能会问: 为什么?
第一个动机是普遍的 JavaScript 包。Webpack 已经将我们所有的 ES6 模块转换为自己定制的 ES5-兼容 模块语法。正如我们简要介绍的, 它将我们所有的 JavaScript 模块封装在特殊功能中。它提供了一个模块 ID 系统, 使一个模块能够引用另一个。
Webpack 和其他打包器一样, 将我们所有的 JavaScript 模块整合到一个文件中。它 可能 将 JavaScript 模块放在单独的文件中, 但是这需要比create-react-app
提供更多的配置。
然而, Webpack 比其他打包器更重视这个模块范例。正如我们所看到的, 它适用于图像资产, CSS 和 npm 包 (如React和 ReactDOM) 相同的模块化处理。这种模块化范式释放了大量的力量。在本章的其余部分, 我们将讨论这一权力的各个方面。
复杂, 对不对?
如果你不明白这一点没关系建立和维护 webpack 是一个复杂的项目, 有大量的移动部件, 它往往需要即使是最有经验的开发商而 “得到”。
我们将遍历我们将使用我们的 webpack 配置的不同部分,。如果它感觉压倒性, 只是坚持我们的基础上, 其余的将遵循。
随着我们对 Webpack 内部运作的新认识, 让我们把注意力转向我们的应用。我们将对我们的 webpack 构建工具进行一些修改, 以支持多种环境配置。
环境配置
当我们准备好部署一个新的应用时, 我们必须考虑一些我们在开发应用时不必关注的事情。
例如, 假设我们正在请求 api 服务器的数据…… 在开发此应用时, 我们可能会在本地计算机上运行 API 服务器的开发实例 (可通过localhost
访问)。
当我们部署应用时, 我们希望从外部主机请求数据, 很可能不在发送代码的位置上, 所以localhost
只是不能做到。
我们能够处理配置管理的一种方法是使用 .env
文件 。这些 .env
文件将包含不同的变量, 为我们不同的条件, 但仍然提供了我们处理配置的正常方式的一种方式,。
通常情况下, 我们将在根目录中保留一个.env
文件, 以包含一个 全局 配置, 可以在每个基础上按条件将其重写。
让我们安装一个称为dotenv
的npm
程序包, 以帮助我们进行此配置设置,
npm install --save-dev dotenv
dotenv 库帮助我们将环境变量加载到我们的环境中的应用的 ENV
中。
添加
.env
到我们的.gitignore
文件通常是一个好主意, 所以我们不签入这些设置。传统上, 创建一个
.env
文件的示例版本是一个好主意,。例如, 对于我们的应用, 我们可以创建一个名为.env.example
的必须变量。稍后, 另一个开发人员 (或我们, 几个月后) 可以使用
.env.example
文件作为.env
文件应该是什么样的模板。
这些.env
文件可以包含变量, 就好像它们是 unix 样式的变量一样。让我们创建一个全局的变量APP_NAME
设置为30days:
touch .env
echo "APP_NAME=30days" > .env
让我们浏览到爆炸的config/
目录, 在那里我们将看到为我们写的我们所有的构建工具。我们不会查看所有这些文件, 但是为了了解 什么 的情况, 我们将开始查找config/webpack.config.dev.js
。
此文件显示了用于构建我们的应用的所有 webpack 配置。它包括装载、插件、入口点等。对于我们当前的任务, 要查找的行是在 plugins
列表中定义 DefinePlugin()
:
module.exports = {
// ...
plugins: [
// ...
// Makes some environment variables available to
// the JS code, for example:
// if (process.env.NODE_ENV === 'development') {
// ...
// }. See `env.js`
new webpack.DefinePlugin(env),
// ...
]
}
webpack.DefinePlugin
插件采用了一个带有 “键” 和 “值” 的对象, 并在我们的代码中找到了我们使用”键”的所有位置, 并将它替换为值。
例如, 如果 env
对象看起来像:
{
'__NODE_ENV__': 'development'
}
我们可以在我们的源使用变量__NODE_ENV__
, 它将被替换为 ‘development’, 即:
class SomeComponent extends React.Component {
render() {
return (
<div>Hello from {__NODE_ENV__}</div>
)
}
}
render()
函数的结果会说 “Hello from development”。
要将我们自己的变量添加到我们的应用中, 我们将使用这个env
对象, 并添加我们自己的定义。向上滚动到文件顶部, 我们将看到它当前是从 config/env.js
文件中创建和导出的。
看着 config/env.js
文件, 我们可以看到, 它将所有的变量都放在我们环境, 并将NODE_ENV
添加到环境中, 以及任何以 REACT_APP_
为前缀的变量。
// ...
module.exports = Object
.keys(process.env)
.filter(key => REACT_APP.test(key))
.reduce((env, key) => {
env['process.env.' + key] = JSON.stringify(process.env[key]);
return env;
}, {
'process.env.NODE_ENV': NODE_ENV
});
我们可以跳过该操作的所有复杂部分, 因为我们只需要修改第二个参数以减少函数, 换句话说, 我们将更新对象:
{
'process.env.NODE_ENV': NODE_ENV
}
该对象是归并函数的_初始_对象。
reduce
函数将所有以REACT_APP_
为前缀的变量_合并_到此对象中,所以我们总是在我们的源代码中替换process.env.NODE_ENV
。
基本上我们要做的是:
加载我们的默认
.env
文件加载任何环境的
.env
文件将这两个变量以及任何默认变量(如
NODE_ENV
)合并在一起我们将创建一个包含所有环境变量的新对象,并对每个值进行清理。
更新现有环境创建者的初始对象。
让我们忙吧 为了加载.env
文件,我们需要导入dotenv
包。 我们还将从标准节点库导入path
库,并为路径设置一些变量。
Let’s update the config/env.js
file我们来更新config / env.js
文件
var REACT_APP = /^REACT_APP_/i;
var NODE_ENV = process.env.NODE_ENV || 'development';
const path = require('path'),
resolve = path.resolve,
join = path.join;
const currentDir = resolve(__dirname);
const rootDir = join(currentDir, '..');
const dotenv = require('dotenv');
要加载全局环境,我们将使用dotenv
库公开的config()
函数,并传递根目录中加载的.env
文件的路径。 我们还将使用相同的功能在config/
目录中查找名称为NODE_ENV.config.env
.的文件。 此外,我们不希望这些方法之一出错,所以我们将添加一个silent: true
的附加选项,以便如果找不到该文件,则不会抛出异常。
// 1\. Step one (loading the default .env file)
const globalDotEnv = dotenv.config({
path: join(rootDir, '.env'),
silent: true
});
// 2\. Load the environment config
const envDotEnv = dotenv.config({
path: join(currentDir, NODE_ENV + `.config.env`),
silent: true
});
接下来, 让我们将所有这些变量串联在一起, 并在这个对象中包括我们的 NODE_ENV
选项。Object.assign()
方法创建一个 新 对象, 并从右向左合并每个对象。这样, 环境配置变量
const allVars = Object.assign({}, {
'NODE_ENV': NODE_ENV
}, globalDotEnv, envDotEnv);
使用当前的设置, allVars
变量的外观将如下所:
{
'NODE_ENV': 'development',
'APP_NAME': '30days'
}
最后, 让我们创建一个将这些变量放在 process.env
中的对象, 并确保它们是有效的字符串 (使用JSON.stringify
)。
const initialVariableObject =
Object.keys(allVars)
.reduce((memo, key) => {
memo['process.env.' + key.toUpperCase()] =
JSON.stringify(allVars[key]);
return memo;
}, {});
使用我们当前的设置(在根目录中有.env
文件),这将创建initialVariableObject
为以下对象:
{
'process.env.NODE_ENV': '"development"',
'process.env.APP_NAME': '"30days"'
}
现在, 我们可以使用这个 initialVariableObject
作为原始module.exports
的第二个参数。让我们更新它以使用这个对象:
module.exports = Object
.keys(process.env)
.filter(key => REACT_APP.test(key))
.reduce((env, key) => {
env['process.env.' + key] = JSON.stringify(process.env[key]);
return env;
}, initialVariableObject);
现在, 我们的代码中的任何位置都可以使用我们在 .env
文件中设置的变量。
由于我们正在向我们的应用中的离线站点发出请求, 让我们使用我们的新配置选项来更新此主机。
假设默认情况下, 我们希望将 TIME_SERVER 设置为 http://localhost:3001
,这样, 如果在环境配置中不设置TIME_SERVER
,它将默认为本地主机。我们可以通过将TIME_SERVER
变量添加到全局 “。
让我们更新 .env
文件, 使其包括此时间服务器:
APP_NAME=30days
TIME_SERVER='http://localhost:3001'
现在, 我们已经开发的 “开发” 与服务器托管在 heroku。我们可以设置我们的config/development.config.env
文件, 以设置 TIME_SERVER
变量, 它将覆盖全局项:
TIME_SERVER='https://fullstacktime.herokuapp.com'
现在, 当我们运行npm start
时, 任何出现的process.env.TIME_SERVER
将被替换为优先值。
让我们更新我们的src/redux/modules/currentTime.js
模块来使用新的服务器, 而不是我们以前使用的硬编码的。
// ...
export const reducer = (state = initialState, action) => {
// ...
}
const host = process.env.TIME_SERVER;
export const actions = {
updateTime: ({timezone = 'pst', str='now'}) => ({
type: types.FETCH_NEW_TIME,
meta: {
type: 'api',
url: host + '/' + timezone + '/' + str + '.json',
method: 'GET'
}
})
}
现在, 对于我们的生产部署, 我们将使用 heroku 应用, 因此, 让我们在config/
下创建development.config.env
的一份拷贝为production.config.env
。
cp config/development.config.env config/production.config.env
每个配置环境自定义中间件
我们在应用中使用了自定义日志再现中间件。这对于在我们的开发站点上工作是非常棒的, 但是我们并不希望它在生产环境中处于活动状态。
让我们更新我们的中间件配置, 在开发时只使用日志中间件, 而不是在所有环境中。在我们的项目的src/redux/configureStore.js
文件中, 我们用一个简单的数组加载了我们的中间件:
let middleware = [
loggingMiddleware,
apiMiddleware
];
const store = createStore(reducer, applyMiddleware(...middleware));
现在, 我们在我们的文件中有了 process.env.NODE_ENV
, 我们可以更新middleware
数组, 这取决于我们正在运行的环境。让我们更新它, 如果我们在开发环境中只添加日志记录,:
let middleware = [apiMiddleware];
if ("development" === process.env.NODE_ENV) {
middleware.unshift(loggingMiddleware);
}
const store = createStore(reducer, applyMiddleware(...middleware));
现在, 当我们运行应用的开发, 我们将有loggingMiddleware
设置, 而在任何其他环境中, 我们已经禁用它。
今天是一个漫长的, 但明天是一个激动人心的一天, 因为我们将得到应用和运行在远程服务器上。
今天的工作很棒, 明天见!