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
自动打开网页:
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
一级目录结构很简单,我们主要分析一下 public
和 src
目录
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>
,即挂载的根元素
Tip:robots.txt
即robots协议,只是约定俗成的,所以并不能保证网站的隐私
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 应该是根组件。
上面我们启动 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
文件中。
组件创建有两种方式:函数组件以及类组件。
这里的 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.js
或 Http.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
目录。
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': ''
}
}))
};
Tip:http-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
就会显示该文件路径。
- vscode 中将鼠标移至
- 用法上从
proxy
改为createProxyMiddleware
。- 我们的版本是 2.0.4,也是此刻官网最新版,其用法使用的就是
createProxyMiddleware
- 我们的版本是 2.0.4,也是此刻官网最新版,其用法使用的就是
// 本地版本
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项目 系列