本文轉載自:眾成翻譯
譯者: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
設定, 而在任何其他環境中, 我們已經禁用它。
今天是一個漫長的, 但明天是一個激動人心的一天, 因為我們將得到應用和執行在遠端伺服器上。
今天的工作很棒, 明天見!