七天接手react專案 系列 —— react 腳手架建立專案

彭加李發表於2022-03-20

其他章節請看:

七天接手react專案 系列

react 腳手架建立專案

前面我們一直通過 script 的方式學習 react 基礎知識,而真實專案通常是基於腳手架進行開發。

本篇首先通過 react 腳手架建立專案,分析其目錄結構,接著編寫第一個元件、解決樣式覆蓋,最後配置代理 proxy 以及通過訊息釋出與訂閱解決兄弟元件之間的通訊問題。

Tip:我們要接手的 react 專案是:spug_web

使用 react 腳手架建立專案 react-cli-demo

前面我們學習 vue 腳手架 vue-cli 建立一個專案是這樣:

> vue create vue-hello-world

在 react 中建立專案是這樣:

$ npx create-react-app react-cli-demo

Creating a new React app in exercise\react-cli-demo.

Installing packages. This might take a couple of minutes.
Installing react, react-dom, and react-scripts with cra-template...


added 1368 packages in 2m

169 packages are looking for funding
  run `npm fund` for details

Initialized a git repository.

Installing template dependencies using npm...
npm WARN deprecated source-map-resolve@0.6.0: See https://github.com/lydell/source-map-resolve#deprecated

added 38 packages in 9s

169 packages are looking for funding
  run `npm fund` for details
Removing template package using npm...


removed 1 package, and audited 1406 packages in 4s

169 packages are looking for funding
  run `npm fund` for details

6 moderate severity vulnerabilities

To address all issues (including breaking changes), run:
  npm audit fix --force

Run `npm audit` for details.

Created git commit.

Success! Created react-cli-demo at exercise\react-cli-demo
Inside that directory, you can run several commands:

  npm start
    Starts the development server.

  npm run build
    Bundles the app into static files for production.

  npm test
    Starts the test runner.

  npm run eject
    Removes this tool and copies build dependencies, configuration files
    and scripts into the app directory. If you do this, you can't go back!

We suggest that you begin by typing:

  cd react-cli-demo
  npm start

Happy hacking!

Create React App 是一個用於學習 React 的舒適環境,也是用 React 建立新的單頁應用最佳方式 —— 官網-Create React App

$ cd react-cli-demo/

本地啟動專案:

$ npm start

> react-cli-demo@0.1.0 start
> react-scripts start

(node:3880) [DEP_WEBPACK_DEV_SERVER_ON_AFTER_SETUP_MIDDLEWARE] DeprecationWarning: 'onAfterSetupMiddleware' option is deprecated. Please use the 'setupMiddlewares' option.
(Use `node --trace-deprecation ...` to show where the warning was created)
(node:3880) [DEP_WEBPACK_DEV_SERVER_ON_BEFORE_SETUP_MIDDLEWARE] DeprecationWarning: 'onBeforeSetupMiddleware' option is deprecated. Please use the 'setupMiddlewares' option.
Starting the development server...

Compiled successfully!

You can now view react-cli-demo in the browser.

  Local:            http://localhost:3000
  On Your Network:  http://192.168.85.1:3000

Note that the development build is not optimized.
To create a production build, use npm run build.

assets by path static/ 1.49 MiB
  asset static/js/bundle.js 1.48 MiB [emitted] (name: main) 1 related asset
  asset static/js/node_modules_web-vitals_dist_web-vitals_js.chunk.js 6.93 KiB [emitted] 1 related asset
  asset static/media/logo.6ce24c58023cc2f8fd88fe9d219db6c6.svg 2.57 KiB [emitted] (auxiliary name: main)
asset index.html 1.67 KiB [emitted]
asset asset-manifest.json 546 bytes [emitted]
cached modules 1.37 MiB (javascript) 31.3 KiB (runtime) [cached] 122 modules
webpack 5.69.1 compiled successfully in 1867 ms

自動開啟網頁:
react-cli-demo-1

react-cli-demo 目錄結構分析

exercise\react-cli-demo> dir

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
d-----          2022/3/3     17:50                node_modules
d-----          2022/3/3     17:48                public
d-----         2022/3/18     19:22                src
-a----        1985/10/26     16:15            310 .gitignore
-a----          2022/3/3     17:49        1120931 package-lock.json
-a----          2022/3/3     17:49            817 package.json
-a----        1985/10/26     16:15           3359 README.md

一級目錄結構很簡單,我們主要分析一下 publicsrc 目錄

public 目錄

public> dir

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a----        1985/10/26     16:15           3870 favicon.ico
-a----          2022/3/4     16:06           1966 index.html
-a----        1985/10/26     16:15           5347 logo192.png
-a----        1985/10/26     16:15           9664 logo512.png
-a----        1985/10/26     16:15            492 manifest.json
-a----        1985/10/26     16:15             67 robots.txt

從中我們猜測主要檔案應該是 index.html。內容如下:

// 註釋已全部刪除

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="utf-8" />
  <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <meta name="theme-color" content="#000000" />
  <meta name="description" content="Web site created using create-react-app" />
  <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
  <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
  <title>React App</title>
</head>

<body>
  <noscript>You need to enable JavaScript to run this app.</noscript>
  <div id="root"></div>
</body>

</html>

index.html核心就是 <div id="root"></div>,即掛載的根元素

Tiprobots.txtrobots協議,只是約定俗成的,所以並不能保證網站的隱私

src 目錄

src> dir

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
d-----          2022/3/4     16:56                components
-a----        1985/10/26     16:15            564 App.css
-a----         2022/3/18     19:22            553 App.js
-a----        1985/10/26     16:15            246 App.test.js
-a----        1985/10/26     16:15            366 index.css
-a----        1985/10/26     16:15            500 index.js
-a----        1985/10/26     16:15           2632 logo.svg
-a----        1985/10/26     16:15            362 reportWebVitals.js
-a----        1985/10/26     16:15            241 setupTests.js
index.js 和 App.js

哪個是入口檔案?App.js 還是 index.js?我們先看一下這兩個檔案的內容:

// index.js 已刪除註釋

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
);

reportWebVitals();
// App.js
import logo from './logo.svg';
import './App.css';

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          Edit <code>src/App.js</code> and save to reload.
        </p>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </header>
    </div>
  );
}

export default App;

index.js 引用了 App.js

我們在回憶一下 vue-cli 生成的專案,也有 App.js 檔案,不過是被 main.js 引用。內容如下:

// main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'

Vue.config.productionTip = false

new Vue({
  router,
  store,
  render: h => h(App)
}).$mount('#app')
// App.js
<template>
  <div id="app">
    <div id="nav">
      <router-link to="/">Home</router-link> |
      <router-link to="/about">About</router-link>
    </div>
    <router-view/>
  </div>
</template>

對比發現,都是將 App 元件掛載到 dom 上 —— react 是 #root,vue 是 #app

至此,我們知道 index.js入口檔案,而 App 應該是根元件。

App.css

上面我們啟動 react-cli-demo 專案,網頁中有這麼一句話:

Edit src/App.js and save to reload.

編輯 src/App.js 並儲存以重新載入。

App.js 中有 import './App.css';,筆者嘗試修改一下 App.css

.App-header {
- background-color: #282c34;
+ background-color: orange;
  ...
}

儲存後,發現頁面背景自動變成橙色

於是我們知道 App.css 應該是 App 元件的樣式。而 vue 中樣式、html和 js 都在一個 .vue 檔案中。

第一個元件 HelloWorld

元件建立有兩種方式:函式元件以及類元件。

這裡的 App.js 使用的是函式元件,我們也用函式的方式建立元件,然後讓 App 載入。請看示例:

// App.js
// webpack 中就可以省略 .js
import HelloWorld from './HelloWorld.js';

export default function App() {
  return (
    <div className="App">
      < HelloWorld />
    </div >
  );
}
// HelloWorld.js
export default function HelloWorld() {
    return <div>hello world!</div>
}

頁面顯示 hello world!

對應類元件的實現:

import { Component } from 'react'
export default class HelloWorld extends Component {
    render() {
        return <div>hello world!</div>
    }
}

字尾js/jsx

我們寫的元件 HelloWorld 其實是 jsx 語法,所以可以將 HelloWorld.js 改為 HelloWorld.jsx,引入的字尾名也同步一下即可。

import HelloWorld from './HelloWorld.jsx';

Tip:純邏輯的 js 可以用 .js 或小寫(例如 http.js),元件用 Http.jsHttp.jsx

Create React App 在內部使用 webpack,而 webpack 能夠使使用者在引入模組時不帶擴充套件:

import File from '../path/to/file';

經測試,無論是 HelloWorld.js 還是 HelloWorld.jsx,都可以省略副檔名引入:

import HelloWorld from './HelloWorld';

components

src/App.js 作為元件的根元件(或元件殼子),現在我們的HelloWorld 元件和它是同一目錄,倘若以後元件變多了,豈不是不好管理,所以我們可以將元件放在 src/components 資料夾中。請看實現:

將 HelloWorld 元件程式碼移至 index.js

// src/components/HelloWorld/index.js

export default function HelloWorld() {
    return <div>hello world!</div>
}

App.js 中修改引入元件的程式碼:

// 預設會去載入 HelloWorld 資料夾中的 index.js或 index.jsx
import HelloWorld from './components/HelloWorld';

...

樣式覆蓋

假如我在 App 中引入兩個元件,每個元件有自己的樣式,讓若發生衝突怎麼辦?請看示例:

// App.js
import HelloWorld from './components/HelloWorld'
import HelloWorld2 from './components/HelloWorld2'

export default function App() {
  return (
    <div className="App">
      < HelloWorld />
      < HelloWorld2 />
    </div >
  );
}

元件1 的文字是藍色

import './index.css'

export default function HelloWorld() {
    return <div className="title">hello world!</div>
}
.title{color:blue}

元件2與元件1相同,唯一區別是文字顏色為紅色

.title{color:red}

最終,頁面中兩個元件的文字顏色都是紅色

頁面有如下程式碼:

<style>.title{color:blue}
</style>
<style>.title{color:red}
</style>

<div class="title">hello world!</div>
<div class="title">hello world!</div>

由此我們知道後者樣式將前者給覆蓋了。

樣式模組化

我們可以使用樣式模組化來修復樣式覆蓋的問題。

比如我要將 HelloWorld 元件樣式模組化,只需要兩步:

首先重新命名樣式檔案。在名字和 css 之間增加 module

component/HelloWorl/index.css

// 重新命名後
component/HelloWorl/index.module.css

然後使用樣式的方式也得調整。就像這樣:

import helloWorld from './index.module.css'

export default function HelloWorld() {
    return <div className={helloWorld.title}>hello world!</div>
}

最終,頁面中兩個元件的文字顏色分別是藍色紅色,與預期相符。

頁面有如下程式碼:

<style>.HelloWorld_title__kRYA7{color:blue}
</style>
<style>.title{color:red}
</style>

<div class="HelloWorld_title__kRYA7">hello world!</div>
<div class="title">hello world!</div>

Tip:效果其實和 vue 中 Scoped Css 類似

less

我們還可以使用 less 這類 css 預處理語言來避免樣式衝突。就像這樣:

.HelloWorld {
  .title{color: blue}
}
.HelloWorld2 {
  .title{color: red}
}

代理 Proxy

在 vue-cli 中我們曾使用 proxy 做過一個需求:新建一個頁面,裡面有 2 個按鈕,點選按鈕能發出相應的請求,一個是非跨域請求,一個是跨域請求。

這裡我們也實現一下。無需按鈕,之間在元件中發請求即可。

在 React 開發中,你能使用任何你喜歡的 AJAX 庫,比如社群比較流行的 Axios,jQuery AJAX,或者是瀏覽器內建的 window.fetch —— 官網-如何在 React 中發起 AJAX 請求?

Tip:React 和 Vue 將注意力集中保持在核心庫,而將其他功能如ajax、路由和全域性狀態管理交給相關的庫。

axios

我們曾在 vue-loader 擴充套件 這裡將 axios 整合到專案中。這裡卻無需那麼複雜,只需能發出 ajax 請求。

根據 axios 官網 介紹,簡單使用只需兩步:

首先下載依賴包:

react-cli-demo> npm i axios

added 1 package, and audited 1407 packages in 7s

169 packages are looking for funding
  run `npm fund` for details

6 moderate severity vulnerabilities

To address all issues (including breaking changes), run:
  npm audit fix --force

Run `npm audit` for details.

在 HelloWorld 元件中通過 axios 發起一個 ajax 請求:

// src/components/HelloWorld/index.jsx

const axios = require('axios');

export default function HelloWorld() {
    axios.get('/index.html')
        .then(function (response) {
            console.log(response.data);
        })

    return <div>hello world!</div>
}

瀏覽器控制檯輸出:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="utf-8" />
  <link rel="icon" href="/favicon.ico" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <meta name="theme-color" content="#000000" />
  <meta name="description" content="Web site created using create-react-app" />
  <link rel="apple-touch-icon" href="/logo192.png" />
  <link rel="manifest" href="/manifest.json" />
  <title>React App</title>
<script defer src="/static/js/bundle.js"></script></head>

<body>
  <noscript>You need to enable JavaScript to run this app.</noscript>
  <div id="root"></div>
</body>

</html>

請求將 react-cli-demo/public/index.html 的內容返了回來,於是我們知道本地伺服器的根是 public 目錄。

setupProxy.js

spug_web 中有個叫 setupProxy.js 的檔案,內容如下:

// src/setupProxy.js
const proxy = require('http-proxy-middleware');

module.exports = function (app) {
  app.use(proxy('/api/', {
    target: 'http://127.0.0.1:8000',
    changeOrigin: true,
    ws: true,
    headers: {'X-Real-IP': '1.1.1.1'},
    pathRewrite: {
      '^/api': ''
    }
  }))
};

Tiphttp-proxy-middleware - http 代理中介軟體。

對比在 vue 中做 proxy 代理,程式碼非常相似:

// vue.config.js
module.exports = {
  devServer: {
    proxy: {
      // 只有 /pengjiali 的請求會被代理
      '/pengjiali': {
        target: 'https://www.cnblogs.com/',
        // changeOrigin: true
      },
    }
  }
}

接下來我們就依葫蘆畫瓢:

新建 setupProxy.js

// src/setupProxy.js

const { createProxyMiddleware } = require('http-proxy-middleware');

module.exports = function (app) {
  app.use(
    // 將原來的 proxy 改為 createProxyMiddleware 
    createProxyMiddleware(
      '/pengjiali',
      {
        target: 'https://www.cnblogs.com/',
        changeOrigin: true
      }
    )
  )
}
// src/components/HelloWorld/index.jsx

const axios = require('axios');

export default function HelloWorld() {
    axios.get('/pengjiali/p/14561119.html')
        .then(function (response) {
            // handle success
            console.log(response.data);
        }).catch(function (error) {
            // handle error
            console.log(error);
        })

    return <div>hello world2!</div>
}

重啟服務,控制檯輸出博文內容。

http-proxy-middleware 有兩點和 spug_web 不同:

  • 筆者這版的react腳手架預設已有 http-proxy-middleware,所以我們無需在下載。
    • vscode 中將滑鼠移至 http-proxy-middleware 就會顯示該檔案路徑。
  • 用法上從 proxy 改為 createProxyMiddleware
    • 我們的版本是 2.0.4,也是此刻官網最新版,其用法使用的就是 createProxyMiddleware
// 本地版本
react-cli-demo/node_modules/http-proxy-middleware (master)
$ cat package.json |head -n 5
{
  "name": "http-proxy-middleware",
  "version": "2.0.4",
  "description": "The one-liner node.js proxy middleware for connect, express and browser-sync",
  "main": "dist/index.js",

訊息訂閱與釋出

在 vue 中我們可以使用中央事件匯流排(或稱 bus)來解決兄弟元件之間的通訊。bus 相當於一箇中介,元件可以在其上訂閱訊息,當觸發時就會將訊息通知到訂閱者。其原理其實就是訊息訂閱與釋出。

react 可以通過 pubsub-js 包來實現元件之間通訊。

Tip:PubSubJS 是一個用 JavaScript 編寫的基於主題的釋出/訂閱庫 —— pubsub-js

下面我們定義兩個元件,元件1訂閱訊息,元件2釋出訊息。請看實現:

首先按照依賴包:

react-cli-demo> npm i pubsub-js

added 1 package, and audited 1408 packages in 5s

169 packages are looking for funding
  run `npm fund` for details

6 moderate severity vulnerabilities

To address all issues (including breaking changes), run:
  npm audit fix --force

Run `npm audit` for details.

接著定義兩個元件:

// 元件1訂閱訊息
// src/components/HelloWorld/index.jsx

import PubSub from 'pubsub-js'

export default function HelloWorld() {
    // 訂閱 message1
    PubSub.subscribe('msg1', function (msg, data) {
        console.log(msg, data);
    });

    return <div>hello world!</div>
}
// 元件2釋出訊息
// src/components/HelloWorld2/index.jsx

import PubSub from 'pubsub-js'

export default function HelloWorld() {
    PubSub.publish('msg1', '旅遊去');
    return <div>hello world2!</div>
}

頁面控制檯顯示:msg1 旅遊去

其他章節請看:

七天接手react專案 系列

相關文章