搭建 react 項目

從0梳理 react 單頁項目搭建。css

懶惰的官網內容搬運工,如有不解,請訪問官網原文。html


源碼地址:github.com/zzyper/buil…前端

參考資源:node

webpack webpack.js.org/react

webpack 中文 webpack.docschina.org/webpack

babel babeljs.io/ios

babel 中文 www.babeljs.cn/nginx

從零搭建React全家桶框架教程 github.com/brickspert/…git

其餘參考資源在相應章節中指出github

文章中各個 npm 包版本請參考 package.json


內容結構

1、認識 webpack

1.1 在項目中安裝使用 webpack

1.2 使用配置文件控制 webpack
複製代碼

2、認識 babel

經過使用 babel 轉換 ES2015+ 語法,來學習它的基本知識
複製代碼

3、管理資源

3.1 趁熱打鐵,使用 webpack + babel 編譯、打包 react 。

3.2 其餘資源管理(比較簡單,後續章節介紹)
複製代碼

4、管理輸出

4.1 使用插件 CleanWebpackPlugin (打包時清除 dist 目錄下舊版本文件)

4.2 使用插件 HtmlWebpackPlugin (html 自動引入打包後的 js 文件)
複製代碼

5、分離配置文件

分離開發環境與生產環境的配置文件。
複製代碼

6、開發環境配置

6.1 使用 source-map

6.2 使用 WebpackDevServer (本地 server)

6.3 使用 HotModuleReplacement (熱模塊替換)
複製代碼

7、管理資源後續

7.1 sass-loader

7.2 postcss-loader

7.3 使用插件 MiniCssExtractPlugin ,抽取 css 文件

7.4 font 處理

7.5 image 處理
複製代碼

8、引入 react-router

8.1 安裝使用 react-router

8.2 react-router 代碼分割/按需加載
複製代碼

9、引入 redux

9.1 安裝使用 redux

9.2 使用 redux-sage 處理異步 action
複製代碼

10、生產環境部署與配置


建立項目

新建 build-react 做爲項目根目錄,建立 src 目錄。

執行命令:

# 初始化項目,生成 package.json ( -y 指默認參數,不用在命令行輸入那些信息)
npm init -y
複製代碼

建立項目

1、認識 webpack

1.1 在項目中安裝使用 webpack

1. 在項目中安裝 webpack

cnpm i webpack --save-dev
cnpm i webpack-cli --save-dev # 用於在命令行中運行 webpack
複製代碼

你可能想知道 --save-dev--save 的區別,自行查找。

2. 在 src 下建立 index.js

// index.js

console.log('Hello webpack !');
複製代碼

3. 使用 webpack 打包

./node_modules/.bin/webpack --mode production
複製代碼

執行以後,咱們的到了 /dist 目錄,以及它下面的 main.js 文件。經過觀察能夠看到 main.js 尾部就是咱們的 index.js

webpack打包結果

你可能想知道:

a. 爲何不直接使用 webpack 而是使用 ./node_modules/.bin/webpack ?

由於若是你全局安裝了 `webpack` ,那麼直接使用 `webpack` 執行的是你全局安裝的,而不是項目下的。
複製代碼

b. --mode 是什麼?

webpack 4 新增的一項配置,能夠用來表示當前打包環境,它會在內部根據這個參數作一些插件的默認使用配置等。
這裏只是不想看到控制檯打印 Warning。
複製代碼

4. 在 package.json 中添加指令"build": "webpack --mode production",方便咱們執行打包

{
  "name": "build-react",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack --mode production"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "webpack": "^4.29.6",
    "webpack-cli": "^3.3.0"
  }
}

複製代碼

接下來,咱們執行 npm run build 就至關於 ./node_modules/.bin/webpack --mode production

5. 從上述過程當中能夠看出,webpack 默認將 /src/index.js 打包到 ./dist/main.js。如今,咱們建立 loading.js,並在 index.js 中導入它,再次執行打包。

// loading.js
export default () => {
  console.log('loading');
};
複製代碼
// index.js
import loading from './loading';

loading();
console.log('Hello webpack !');
複製代碼

查看打包過程及結果,能夠看到它們都被打包到 main.js

打包結果

1.2 使用配置文件控制 webpack

在 webpack 4 中,能夠無須任何配置使用,然而大多數項目會須要很複雜的設置,這就是爲何 webpack 仍然要支持 配置文件。這比在終端(terminal)中手動輸入大量命令要高效的多,因此讓咱們建立一個取代以上使用 CLI 選項方式的配置文件。

1. 在項目目錄下建立 webpack.config.js

// webpack.config.js
const path = require('path');

module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist')
  }
};
複製代碼

2. 在 package.json 下修改指令

"build": "webpack --config webpack.config.js --mode production"
複製代碼

若是 webpack.config.js 存在,則 webpack 命令將默認選擇使用它。咱們在這裏使用 --config 選項只是向你代表,能夠傳遞任何名稱的配置文件。這對於須要拆分紅多個文件的複雜配置是很是有用,對於後面將要提到的分離開發/生產環境配置也十分有用。

3. 執行打包 npm run build

咱們能夠看到 ./dist 下產生了咱們在配置文件中指定的輸出文件 bundle.js

打包結果

比起 CLI 這種簡單直接的使用方式,配置文件具備更多的靈活性。咱們能夠經過配置方式指定 loader 規則(loader rules)、插件(plugins)、解析選項(resolve options),以及許多其餘加強功能。瞭解更多詳細信息,請查看配置文檔。

上面所說的這些配置就是 webpack 的一些核心概念,在官網都有詳細的解釋。

概念 配置

2、認識 babel

Babel是什麼?

咱們經過使用 babel 編譯 ES2015 + jsx 等等代碼,總有人認爲它是 webpack 的一部分,實際上它和 webpack 並沒有關係,接下來咱們跟着使用指南,經過編譯 ES2015+ 來了解它。

1. 安裝核心庫,使用 babel 的基石

cnpm install --save-dev @babel/core
複製代碼

2. 安裝使用 CLI 命令行工具

cnpm install --save-dev @babel/core @babel/cli

./node_modules/.bin/babel src --out-dir lib
複製代碼

這將解析 src 目錄下的全部 JavaScript 文件,並應用咱們所指定的代碼轉換功能,而後把每一個文件輸出到 lib 目錄下。因爲咱們尚未指定任何代碼轉換功能,因此輸出的代碼將與輸入的代碼相同(不保留原代碼格式)。咱們能夠將咱們所須要的代碼轉換功能做爲參數傳遞進去。

上面的示例中咱們使用了 --out-dir 參數。你能夠經過 --help 參數來查看命令行工具所能接受的全部參數列表。可是如今對咱們來講最重要的是 --plugins 和 --presets 這兩個參數

接下來咱們討論 --plugins 和 --presets ,及插件和預設。

3. 插件和預設(preset)

代碼轉換功能以插件的形式出現,插件是小型的 JavaScript 程序,用於指導 Babel 如何對代碼進行轉換。你甚至能夠編寫本身的插件將你所須要的任何代碼轉換功能應用到你的代碼上。例如將 ES2015+ 語法轉換爲 ES5 語法,咱們可使用諸如 @babel/plugin-transform-arrow-functions 之類的官方插件:

cnpm install --save-dev @babel/plugin-transform-arrow-functions

./node_modules/.bin/babel src --out-dir lib --plugins=@babel/plugin-transform-arrow-functions
複製代碼

咱們來試試:

/src/index.js 內容改成

const fn = () => 1;
複製代碼

執行上述命令 ./node_modules/.bin/babel src --out-dir lib --plugins=@babel/plugin-transform-arrow-functions

babel結果

如今,咱們代碼中的全部箭頭函數(arrow functions)都將被轉換爲 ES5 兼容的函數表達式了。

這是個好的開始!可是咱們的代碼中仍然殘留了其餘 ES2015+ 的特性,咱們但願對它們也進行轉換。咱們不須要一個接一個地添加全部須要的插件,咱們可使用一個 "preset" (即一組預先設定的插件)。

就像插件同樣,你也能夠根據本身所須要的插件組合建立一個本身的 preset 並將其分享出去。J對於當前的用例而言,咱們可使用一個名稱爲 env 的 preset。

cnpm install --save-dev @babel/preset-env

./node_modules/.bin/babel src --out-dir lib --presets=@babel/env
複製代碼

若是不進行任何配置,上述 preset 所包含的插件將支持全部最新的 JavaScript (ES201五、ES2016 等)特性。可是 preset 也是支持參數的。咱們來看看另外一種傳遞參數的方法:配置文件,而不是經過終端控制檯同時傳遞 cli 和 preset 的參數。

4. 配置

www.babeljs.cn/docs/config…

後面咱們都將使用 .babelrc 進行配置,其餘方式自行了解。

5. Polyfill

@babel/polyfill 模塊包括 core-js 和一個自定義的 regenerator runtime 模塊用於模擬完整的 ES2015+ 環境。

這意味着你可使用諸如 Promise 和 WeakMap 之類的新的內置組件、 Array.from 或 Object.assign 之類的靜態方法、 Array.prototype.includes 之類的實例方法以及生成器函數(generator functions)(前提是你使用了 regenerator 插件)。爲了添加這些功能,polyfill 將添加到全局範圍(global scope)和相似 String 這樣的內置原型(native prototypes)中。

對於軟件庫/工具的做者來講,這可能太多了。若是你不須要相似 Array.prototype.includes 的實例方法,可使用 transform runtime 插件而不是對全局範圍(global scope)形成污染的 @babel/polyfill。

更進一步,如哦你確切地指導你所須要的 polyfills 功能,你能夠直接從 core-js 獲取它們。

因爲咱們構建的是一個應用程序,所以咱們只需安裝 @babel/polyfill 便可:

cnpm install --save @babel/polyfill
複製代碼

3、管理資源

假設你已經看了官網相關基礎內容,接下來咱們來結合 webpack babel 來編譯打包 jsx 文件。

3.1 趁熱打鐵,使用 webpack + babel 編譯、打包 react 。

1. 安裝 react,建立 jsx 文件

cnpm install --save react react-dom
複製代碼
// /src/app.jsx
import React from 'react';
import ReactDOM from 'react-dom';

ReactDOM.render(
  <div>Hello react!</div>,
  document.getElementById('root')
);
複製代碼

2. 安裝 babel 用來編譯 react 的預設,配置 babel

cnpm install --save-dev @babel/preset-react
複製代碼
// .babelrc
{
  "presets": [
    "@babel/preset-react"
  ],
  "plugins": []
}
複製代碼

執行 babel 編譯

./node_modules/.bin/babel src --out-dir lib
複製代碼

編譯結果

3. 結合 webpack 編譯並打包 react

假設你已經看了 webpack 官方的基礎內容,你應該知道 webpack 經過各類 loader 來對不一樣資源進行處理,接下來咱們就要使用 babel-loader 來處理 jsx

安裝 babel-loader

cnpm install -D babel-loader
複製代碼

修改 webpack 配置文件

// webpack.config.js
const path = require('path');

module.exports = {
  entry: './src/app.jsx',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist')
  },
  module: {
    rules: [
      {
        test: /\.jsx$/,
        exclude: /(node_modules|bower_components)/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env']
          }
        }
      }
    ]
  }
};
複製代碼

爲了直觀查看結果,咱們在 /dist 下建立 index.html ,引入打包後的 bundle.js

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>
<body>
  <div id="root"></div>
  <script src="./bundle.js"></script>
</body>
</html>
複製代碼

執行打包命令 npm run build,打包結束後,在瀏覽器打開 index.html,大功告成。

結果

3.2 其餘資源管理(比較簡單,後續章節介紹)

關於 css font image 等文件處理,咱們會在第七章進行介紹,相信若是你已經理解了 loader ,併成功編譯打包了 react ,那這些都不在話下。

4、管理輸出

webpack 使用插件,來作那些 loader 作不到的事。

4.1 使用插件 CleanWebpackPlugin (打包時清除 dist 目錄下舊版本文件)

你可能已經注意到,因爲遺留了以前代碼,咱們的 /dist 文件夾顯得至關雜亂。webpack 將生成文件並放置在 /dist 文件夾中,可是它不會追蹤哪些文件是實際在項目中用到的。

一般比較推薦的作法是,在每次構建前清理 /dist 文件夾,這樣只會生成用到的文件。讓咱們實現這個需求。

clean-webpack-plugin 是一個流行的清理插件,安裝和配置它。

cnpm install --save-dev clean-webpack-plugin
複製代碼
// webpack.config.js
const CleanWebpackPlugin = require('clean-webpack-plugin');

plugins: [
    new CleanWebpackPlugin()
]
複製代碼

接下來每次執行打包都會先清除 /dist 下的文件。

4.2 使用插件 HtmlWebpackPlugin (html 自動引入打包後的 js 文件)

細心的小夥伴已經發現,通過剛纔的操做,index.html 也被清除了。

咱們將使用 HtmlWebpackPlugin插件,來生成 html 並將每次打包的js自動插入到你的 index.html 裏面去,並且它還能夠基於你的某個 html 模板來建立最終的 index.html

src 下建立一個模板 tmpl.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>build react</title>
</head>
<body>
  <div id="root"></div>
</body>
</html>
複製代碼

安裝 & 配置:

cnpm install html-webpack-plugin --save-dev
複製代碼
const HtmlWebpackPlugin = require('html-webpack-plugin');

plugins: [
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      filename: 'index.html',
      template: path.join(__dirname, 'src/tmpl.html')
    })
  ]
複製代碼

執行打包,咱們發現,生成的 index.html 自動包含了打包後的js文件,並且也擁有 tmpl.html 模板文件中的內容。

打包結果

5、分離配置文件

整理刪除 lib 等無用的文件目錄。

接下來咱們來分離開發環境與生產環境的配置文件。

每每咱們在開發過程當中須要一些配置在生產環境中並不須要,反之同理,因此咱們要分開配置它們。

  1. 建立 webpack.common.js ,咱們在它其中寫通用的配置;

  2. 建立 webpack.dev.js ,開發環境的配置;

  3. 建立 webpack.prod.js,生產環境的配置。

爲了將這些配置合併在一塊兒,咱們將使用一個名爲 webpack-merge 的工具。此工具會引用 "common" 配置,所以咱們沒必要再在環境特定(environment-specific)的配置中編寫重複代碼。

cnpm install --save-dev webpack-merge
複製代碼
// webpack.common.js
const path = require('path');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  entry: './src/app.jsx',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist')
  },
  module: {
    rules: [
      {
        test: /\.jsx$/,
        exclude: /(node_modules|bower_components)/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env']
          }
        }
      }
    ]
  },
  plugins: [
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      filename: 'index.html',
      template: path.join(__dirname, 'src/tmpl.html')
    })
  ]
};
複製代碼
// webpack.dev.js
const merge = require('webpack-merge');
const common = require('./webpack.common.js');

module.exports = merge(common, {
  mode: 'development',
});
複製代碼
// webpack.prod.js
const merge = require('webpack-merge');
const common = require('./webpack.common.js');

module.exports = merge(common, {
  mode: 'production',
});
複製代碼

細心的同窗已經發現,咱們在 dev prod 中配置了不一樣的 mode ,接下來咱們在 package.json 中配置指令。

"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack --config webpack.prod.js",
    "start": "webpack --config webpack.dev.js"
  },
複製代碼

接下來咱們分別執行 開發環境 npm start 和 生產環境 npm run build

開發環境結果

生產環境結果

比較打包出來的文件體積,我想你已經有點感覺到 mode 的神奇了。

6、開發環境配置

6.1 使用 source-map

當 webpack 打包源代碼時,可能會很難追蹤到 error(錯誤) 和 warning(警告) 在源代碼中的原始位置。例如,若是將三個源文件(a.js, b.jsc.js)打包到一個 bundle(bundle.js)中,而其中一個源文件包含一個錯誤,那麼堆棧跟蹤就會直接指向到 bundle.js。你可能須要準確地知道錯誤來自於哪一個源文件,因此這種提示這一般不會提供太多幫助。

爲了更容易地追蹤 error 和 warning,JavaScript 提供了 source map 功能,能夠將編譯後的代碼映射回原始源代碼。若是一個錯誤來自於 b.js,source map 就會明確的告訴你。

source map 有許多 可用選項,請務必仔細閱讀它們,以即可以根據須要進行配置。

在這裏,咱們將使用 inline-source-map 選項:

// webpack.dev.js

devtool: 'inline-source-map',
複製代碼

6.2 使用 WebpackDevServer (本地 server)

次編譯代碼時,手動運行 npm run build 會顯得很麻煩。

webpack 提供幾種可選方式,幫助你在代碼發生變化後自動編譯代碼:

  1. webpack watch mode(webpack 觀察模式)
  2. webpack-dev-server
  3. webpack-dev-middleware

多數場景中,你可能須要使用 webpack-dev-server,可是不妨探討一下以上的全部選項。

在這裏咱們只用 webpack-dev-server , 其餘兩個推薦看官方文檔,尤爲是 webpack-dev-middleware ,爲帶有 node 中間層的前端應用提供了本地基石。

簡單來講,webpack-dev-server就是一個小型的靜態文件服務器。使用它,能夠爲webpack打包生成的資源文件提供Web服務,而且具備 live reloading(實時從新加載) 功能。

cnpm install --save-dev webpack-dev-server
複製代碼
// webpack.dev.js
const path = require('path');

devServer: {
  port: 8080,
  contentBase: path.join(__dirname, './dist'),
  historyApiFallback: true,
  host: '0.0.0.0'
}
複製代碼
"start": "webpack-dev-server --config webpack.config.dev.js --color --progress"
複製代碼

相關配置:webpack.docschina.org/configurati…

如今咱們來執行 npm start ,訪問 http://0.0.0.0:8080/ ,咱們擁有了本地的服務器,若是你想要同一網段下的其餘機器訪問你的頁面,那還等什麼,去查查 api 怎麼配置吧!

更高級的用法,經過它代理解決開發環境訪問接口跨域問題,自行查找。

6.3 使用 HotModuleReplacement (熱模塊替換)

經過 6.2 的內容,咱們創建了開發環境本地服務器,細心的同窗已經發現,當你修改 app.jsx 內容時,控制檯會從新構建,網頁也會同步刷新。然而,咱們僅僅修改了一處文本,整個頁面也會刷新。本節,咱們來使用 HotModuleReplacement 來實現熱更新,也就是局部內容更新,而不是刷新整個頁面。

模塊熱替換(hot module replacement 或 HMR)是 webpack 提供的最有用的功能之一。它容許在運行時更新全部類型的模塊,而無需徹底刷新。

此功能能夠很大程度提升生產效率。咱們要作的就是更新 webpack-dev-server 配置,而後使用 webpack 內置的 HMR 插件。

// webpack.dev.js
const webpack = require('webpack');

devServer: {
    hot: true
},

plugins: [
    new webpack.HotModuleReplacementPlugin()
],
複製代碼

app.jsx 添加代碼,讓它支持熱模塊替換:

// app.jsx

if (module.hot) {
    module.hot.accept();
}
複製代碼

大功告成!不不不,還太早了,還有個問題咱們須要解決。。

咱們先來改寫 app.jsx ,並建立一個 home.jsx 文件,實現一個計數功能:

// home.jsx
import React from 'react';

export default class Home extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    }
  }

  render() {
    return (
      <div className="home"> <p>count : {this.state.count}</p> <button onClick={() => this.setState({ count: this.state.count += 1 })}>+</button> <button onClick={() => this.setState({ count: this.state.count -= 1 })}>-</button> <button onClick={() => this.setState({ count: 0 })}>reset</button> </div>
    );
  }
}
複製代碼
// app.jsx
import React from 'react';
import ReactDOM from 'react-dom';
import Home from './home.jsx';

ReactDOM.render(
  <Home />, document.getElementById('root') ); if (module.hot) { module.hot.accept(); } 複製代碼

打開頁面,咱們經過按鈕改變 state ,讓它再也不爲 0 ,接下來咱們修改 home.jsx 代碼,發現 state 被初始化了。

當頁面上的 state 再也不是初始值,而代碼內容改動,熱更新會重置 state,而不會保留,這顯然很差。

爲了在react模塊更新的同時,能保留state等頁面中其餘狀態,咱們須要引入react-hot-loader~

1. 安裝 react-hot-loader

cnpm install react-hot-loader --save-dev
複製代碼

2. .babelrc 增長 react-hot-loader/babel

{
  "presets": [
    "@babel/preset-react", // 編譯 react
    "@babel/preset-env" // 編譯 ES2015+
  ],
  "plugins": [
    "react-hot-loader/babel" // react-hot-loader
  ]
}
複製代碼

3. webpack.dev.js 入口增長 react-hot-loader/patch

entry: [
    'react-hot-loader/patch',
    path.join(__dirname, './src/app.jsx')
  ],
複製代碼

4. 修改 app.jsx

import React from 'react';
import ReactDOM from 'react-dom';
import Home from './home.jsx';
import {AppContainer} from 'react-hot-loader';

/*初始化*/
renderWithHotReload(Home);

/*熱更新*/
if (module.hot) {
    module.hot.accept('./home.jsx', () => {
        const Home = require('./home.jsx').default;
        renderWithHotReload(Home);
    });
}

function renderWithHotReload(Home) {
    ReactDom.render(
        <AppContainer> <Home /> </AppContainer>,
        document.getElementById('root')
    )
}
複製代碼

大功告成! :)

7、管理資源後續

7.1 sass-loader

cnpm install css-loader style-loader --save-dev
cnpm install sass-loader node-sass webpack --save-dev
複製代碼
// webpack.common.js
{
  test: /\.scss$/,
    use: [
      "style-loader", // creates style nodes from JS strings
      "css-loader", // translates CSS into CommonJS
      "sass-loader" // compiles Sass to CSS, using Node Sass by default
    ]
}
複製代碼

文件結構

執行結果

7.2 postcss-loader

不知道爲何要用請去這裏:github.com/brickspert/…

cnpm install --save-dev  postcss-loader postcss-cssnext
複製代碼
// webpack.common.js
{
  test: /\.scss$/,
    use: [
      "style-loader",
      "css-loader",
      "postcss-loader",
      "sass-loader"
    ]
}
複製代碼

根目錄增長postcss配置文件。

// postcss.config.js
module.exports = {
    plugins: {
        'postcss-cssnext': {}
    }
};
複製代碼
// home.scss
.home {
  p {
    color: red;
    font-size: 24px;
    transform: scale(1.1);
  }
}
複製代碼

chrome 前綴

7.3 使用插件 MiniCssExtractPlugin ,抽取 css 文件

目前咱們的css是直接打包進js裏面的,咱們但願能單獨生成css文件。

Webpack 4 之前,是用github.com/webpack-con…來實現的,然而:

Since webpack v4 the extract-text-webpack-plugin should not be used for css. Use mini-css-extract-plugininstead.

仍是同樣簡單,但此次咱們只在生產環境中使用,安裝 & 使用:

(開發環境仍然使用 style-loader ,記得移除 webpack.common.js 中的配置)

cnpm install --save-dev mini-css-extract-plugin
複製代碼
// webpack.prod.js
const merge = require('webpack-merge');
const common = require('./webpack.common.js');
const MiniCssExtractPlugin = require("mini-css-extract-plugin");

module.exports = merge(common, {
  mode: 'production',
  module: {
    rules: [
      {
        test: /\.scss$/,
        use: [
          MiniCssExtractPlugin.loader,
          "css-loader",
          "postcss-loader",
          "sass-loader"
        ]
      }
    ]
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: "[name].css",
      chunkFilename: "[id].css"
    })
  ]
});
複製代碼

7.4 font 處理

webpack.docschina.org/guides/asse…

建立 /public/fonts 文件夾,下載字體並使用:

public目錄

@font-face {
  font-family: "ledbdrev";
  src: url("../public/fonts/ledbdrev.ttf");
}

.home {
  p {
    font-family: 'ledbdrev';
    color: red;
    font-size: 24px;
    transform: scale(1.1);
    transform-origin: 0 0;
  }
}
複製代碼

字體展現

7.5 image 處理

github.com/brickspert/…

同字體:

// home.jsx
import React from 'react';
import './home.scss';
import codeImg from '../public/images/code.png'; // 引入圖片

export default class Home extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    }
  }

  render() {
    return (
      <div className="home"> <img src={codeImg} alt="!" /> <p>count : {this.state.count}</p> <button onClick={() => this.setState({ count: this.state.count += 1 })}>+</button> <button onClick={() => this.setState({ count: this.state.count -= 1 })}>-</button> <button onClick={() => this.setState({ count: 0 })}>reset</button> </div> ); } } 複製代碼

處理圖片

8、引入 react-router

react-router是個極簡單的東西,經過點擊這裏,你能夠徹底掌握它。

咱們先來調整一下項目結構:

  1. src 下建立 pages 目錄,在 pages 下建立 home 目錄並把咱們的 home.jsx home.scss 移入其中。 pages 就做爲全部頁面的容器。

  2. pages 下建立 article 文章頁目錄及 article.jsx

  3. 修復相關圖片、字體引用路徑問題。

8.1 安裝使用 react-router

1. 安裝 react-router

cnpm install --save react-router-dom
複製代碼

2. 在 /src 下建立 router.js ;添加 js 文件編譯;在入口 app.jsx 中引入 router.js (替換 HomeRouter)。

import React, { Component } from 'react';
import { Route, Switch } from 'react-router-dom';
import Home from './pages/home/home';
import Article from './pages/home/article';

export default class Router extends Component {
  render() {
    return (
      <div>
        <Switch>
          <Route exact path="/" component={Home} />
          <Route exact path="/article" component={Article} />
        </Switch>
      </div>
    )
  }
}
複製代碼
// webpack.common.js
{
    test: /\.(js|jsx)$/,
    exclude: /(node_modules|bower_components)/,
    use: {
      loader: 'babel-loader',
      options: {
        presets: ['@babel/preset-env']
      }
    }
  },
複製代碼
// /src/app.jsx
import React from 'react';
import ReactDOM from 'react-dom';
import { AppContainer } from 'react-hot-loader';
import { BrowserRouter as Router } from 'react-router-dom'
import Router from './router';

/*初始化*/
renderWithHotReload(Router);

/*熱更新*/
if (module.hot) {
  module.hot.accept('./router.js', () => {
    const Router = require('./router.js').default;
    renderWithHotReload(Router);
  });
}

function renderWithHotReload(Router) {
  ReactDOM.render(
    <AppContainer> <BrowserRouter> <Router /> </BrowserRouter> </AppContainer>,
    document.getElementById('root')
  )
}
複製代碼

3. npm start ,在瀏覽器輸入 url 可訪問 article 文章頁。

文章頁

8.2 react-router 代碼分割/按需加載

咱們項目的全部 js 都被打包到了 bundle.js,打開首頁咱們能夠發現,它加載了 bundle.js,這其中也包含 article 頁面的內容。咱們的項目文件愈來愈大時,打開首屏將會很是緩慢。

接下來咱們來嘗試按需加載(代碼分割)。

webpack 官網能夠看到,代碼分割/按需加載基於動態導入,react-router也提供了相關方案。

reacttraining.com/react-route…

blog.csdn.net/mjzhang1993…

1. 安裝 babel 插件支持動態引入語法,修改 .babelrc;安裝 @loadable/component

cnpm i --save @babel/plugin-syntax-dynamic-import
cnpm i --save @loadable/component
複製代碼
{
  "presets": [
    "@babel/preset-react", // 編譯 react
    "@babel/preset-env" // 編譯 ES2015+
  ],
  "plugins": [
    "react-hot-loader/babel", // react-hot-loader
    "@babel/plugin-syntax-dynamic-import"
  ]
}
複製代碼

2. 新建 /components/loading 目錄及組件,使用 @loadable/component 按需加載組件

// /components/loading.jsx
import React from 'react';

export default () => <div>Loading...</div>
複製代碼
// router.js
import React, { Component } from 'react';
import { Route, Switch } from 'react-router-dom';
import loadable from '@loadable/component'

import Loading from "./components/loading.jsx";

import Home from './pages/home/home.jsx';

const Article = loadable(() => import(/* webpackChunkName: "article" */'./pages/article/article.jsx'), { fallback: <Loading /> });

export default class Router extends Component {
  render() {
    return (
      <div>
        <Switch>
          <Route exact path="/" component={Home} />
          <Route exact path="/article" component={Article} />
        </Switch>
      </div>
    )
  }
}
複製代碼

3. 修改 webpack 配置,讓輸出的 js 有對應的模塊名稱,這裏的 name 其實是咱們在動態導入時,註釋傳入的

output: {
    filename: '[name].js',
    path: path.resolve(__dirname, 'dist')
  },
複製代碼

4. 執行打包,再次進入瀏覽器訪問首頁,而後只有進入文章頁時纔會加載 article.js

本節有個問題:react-hot-loader 熱更新失效。當修改動態引入的組件時,雖然觸發了熱更新,但 dom 並沒有變化。查了 github 相同 issues

在解決上述問題時,搜到了另外一個按需加載方案 react-loadable ,又遇到了渲染的狀態是前一次的問題,原來還要本身在組件裏面寫 hot

import React, { Component } from 'react';
import { hot } from 'react-hot-loader';

class Article extends Component {
  render() {
    return (
      <div> <h1>文章頁111</h1> </div>
    )
  }
}

export default hot(module)(Article);
複製代碼

而後這兩種方法都能用了。。。

9、引入 redux

redux 看上去十分複雜,當你慢慢嘗試使用它,並不斷使用,你徹底不會以爲它是個複雜的技術。

9.1 安裝使用 redux

接下來咱們來使用 redux 來管理應用中的全部狀態。

1. 安裝 redux

cnpm install --save redux react-redux
複製代碼

2. 在首頁下建立 action.js reducer.js ,修改 home.jsx 代碼從 redux store 中獲取狀態以及 dispatch action

// action.js
export const add = () => ({
  type: 'HOME_ADD'
});

export const cut = () => ({
  type: 'HOME_CUT'
});

export const reset = () => ({
  type: 'HOME_RESET'
});
複製代碼
// reducer.js
const initState = {
  count: 0
};

export default (state = initState, aciton) => {
  switch(aciton.type){
    case 'HOME_ADD':
      return {
        count: state.count + 1
      };
    case 'HOME_CUT':
      return {
        count: state.count - 1
      };
    case 'HOME_RESET':
      return {
        count: 0
      };
    default: 
      return state;
  }
}
複製代碼
// home.jsx
import React from 'react';
import { connect } from 'react-redux';
import { add, cut, reset } from './action';

import './home.scss';

class Home extends React.Component {

  constructor(props) {
    super(props);
  }

  render() {
    const { dispatch, count } = this.props;
    return (
      <div className="home"> <h1>這是首頁!</h1> <p>count: {count}</p> <button onClick={() => dispatch(add())}>+</button> <button onClick={() => dispatch(cut())}>-</button> <button onClick={() => dispatch(reset())}>reset</button> </div>
    );
  }
}

export default connect(state => state.home)(Home);
複製代碼

(還記得嗎? article 頁面爲了解決熱更新問題,導出了 hot 包裹的高階組件,而 reudx 又須要包裹 connect ,因此須要導出 export default hot(module)(connect()(Article));)

3. 在 src 目錄下建立 redux 目錄,建立 redux/reducers.js 用來合併全部頁面的 reducer ,建立 redux/store.js 生成 store,最後修改入口 app.jsxredux store 獲取狀態

// reducers.js
import {combineReducers} from "redux";

import home from '../pages/home/reducer';

export default combineReducers({
  home,
});
複製代碼
// store.js
import { createStore } from 'redux';
import combineReducers from './reducers.js';

let store = createStore(
  combineReducers,
  window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);

export default store;
複製代碼
// app.jsx
// /src/app.jsx
import React from 'react';
import ReactDOM from 'react-dom';
import { AppContainer } from 'react-hot-loader';
import { BrowserRouter } from 'react-router-dom'
import { Provider } from 'react-redux';
import store from './redux/store';

import Router from './router';

/*初始化*/
renderWithHotReload(Router);

/*熱更新*/
if (module.hot) {
  module.hot.accept('./router.js', () => {
    const Router = require('./router.js').default;
    renderWithHotReload(Router);
  });
}

function renderWithHotReload(Router) {
  ReactDOM.render(
    <AppContainer> <Provider store={store}> <BrowserRouter> <Router /> </BrowserRouter> </Provider> </AppContainer>,
    document.getElementById('root')
  )
}
複製代碼

9.2 使用 redux-saga 中間件處理異步 action

許多場景下,咱們須要異步的 action ,好比服務器請求。咱們以此爲例,使用一個 redux 中間件 redux-saga 來處理異步 action

幫助文檔 github.com/superRaytin…

1. 安裝 redux-saga

cnpm install --save redux-saga
複製代碼

2. 根目錄建立一個 test-server.jsnode 文件,用來模擬服務器返回數據

// test-server.js

// 加載 HTTP 模塊
const http = require("http");
const hostname = '127.0.0.1';
const port = 5000;

// 建立 HTTP 服務器
const server = http.createServer((req, res) => {
  // 用 HTTP 狀態碼和內容類型(Content-Type)設置 HTTP 響應頭
  res.statusCode = 200;
  res.setHeader('Content-Type', 'application/json');
  res.setHeader('Access-Control-Allow-Origin', '*');
  // 發送響應體
  res.end(JSON.stringify({
    code: 1,
    content: ` 靜夜思 牀前看月光,疑是地上霜。 擡頭望山月,低頭思故鄉。 `
  }));
});

// 監聽 5000 端口的請求,註冊一個回調函數記錄監聽開始
server.listen(port, hostname, () => {
  console.log(`服務器運行於 http://${hostname}:${port}/`);
});
複製代碼

3. /src 下新建一個 service 目錄,用來存放各個頁面的數據請求,建立 service/article.js

4. 引入 axios 發送請求

www.kancloud.cn/yunye/axios…

cnpm install --save axios
複製代碼
// service/article.js
import axios from 'axios';

export const getArticleData = (id) => axios.get(`http://127.0.0.1:5000/?id=${id}`);
複製代碼

5. 在 article 頁面建立請求用的 action reducer 以及 saga.js,並在 reducers 中添加 articlereducer

// action.js
export const reqArticleData = id => ({
  type: 'ARTICLE_GET_DATA',
  id
});

export const setArticleData = content => ({
  type: 'ARTICLE_SET_DATA',
  content
});

export const reqFail = () => ({
  type: 'ARTICLE_REQ_FAIL'
});
複製代碼
// reducer.js
const initState = {
  loading: true,
  content: ''
};

export default (state = initState, action) => {
  switch(action.type){
    case 'ARTICLE_GET_DATA':
      return {
        ...state,
        loading: true
      }
    case 'ARTICLE_SET_DATA':
      return {
        ...state,
        content: action.content,
        loading: false
      }
    case 'ARTICLE_REQ_FAIL':
      return {
        ...state,
        loading: false
      }
    default:
      return state;
  }
}
複製代碼
// saga.js
import { put, takeLatest } from 'redux-saga/effects';
import { getArticleData } from '../../service/article';
import { setArticleData, reqFail } from './action';

function* reqArticleData(action) {
  try {
    const data = yield getArticleData(action.id);
    if (data.data.code === 1) {
      yield put(setArticleData(data.data.content));
    } else {
      yield put(reqFail());
    }
  } catch (e) {
    yield put(reqFail());
  }
}

function* articleSaga() {
  yield takeLatest("ARTICLE_GET_DATA", reqArticleData);
}

export default articleSaga;
複製代碼
// article.jsx
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { hot } from 'react-hot-loader';
import { reqArticleData } from './action';

import Loading from '../../components/loading.jsx';

class Article extends Component {
  componentDidMount() {
    this.props.dispatch(reqArticleData());
  }

  render() {
    return (
      <div> <h1>文章頁</h1> { this.props.loading ? <Loading /> : <pre>{this.props.content}</pre> } </div>
    )
  }
}

export default hot(module)(connect(state => state.article)(Article));
複製代碼

6. 在 store.js 中使用 article/saga.js ,添加 babel-runtime

// store.js
import { createStore, applyMiddleware } from 'redux';
import createSagaMiddleware from 'redux-saga'

import combineReducers from './reducers';
import articleSage from '../pages/article/saga';


const sagaMiddleware = createSagaMiddleware();

let store = createStore(
  combineReducers,
  applyMiddleware(sagaMiddleware)
);

sagaMiddleware.run(articleSage)

export default store;
複製代碼

添加babel-runtime

node test-server.js # 啓動模擬返回數據
npm start
複製代碼

打開文章頁:

文章頁

10、生產環境部署與配置

執行 npm run build 將打包出的 /dist 中的靜態資源部署到服務器,配置 nginx 訪問便可,若遇到二級頁面404的問題,參考以下:

www.jianshu.com/p/5e78477bb…

server {  
   server_name xxx.xxxxxx.com;  
   location / {  
     root /xxx/xxx/xxx/www/build;  
     try_files $uri /index.html;  
   }
}   
複製代碼
相關文章
相關標籤/搜索