create-react-app笔记

基本使用

  • 安装:npm i create-react-app -g

  • 查看版本:create-react-app -V

  • 创建Javascript项目:create-react-app projectname

  • 创建Typescript项目:create-react-app projectname --typescript

  • 弹出配置(单向操作):npm run eject

  • 项目改造

├── package.json
├── public                      # 这个是webpack的配置的静态目录
│   ├── favicon.ico
│   ├── index.html              # 默认是单页面应用,这个是最终的html的基础模板
│   └── manifest.json
├── src
│   ├── assets                  # 图片等静态资源
│   ├── store                   # 状态
│   │      ├── actions.js       # actions
│   │      ├── actionType.js    # action type
│   │      ├── reducers         # reducers目录
│   │      │     ├── index.js   # 根reducer
│   │      │     └── todos.js   # 其他reducer,每个reducer一个文件
│   │      └── index.js         # 主文件
│   ├── router                  # 路由
│   │      ├── config.js        # 配置
│   │      ├── FrontendAuth.js  # 路由守卫
│   │      └── index.js         # 主文件
│   ├── serve                   # 请求
│   │      └── index.js         # axios
│   ├── views                   # 页面
│   ├── App.css                 # App根组件的css
│   ├── App.js                  # App组件代码
│   ├── App.test.js
│   ├── index.css               # 启动文件样式
│   ├── index.js                # 启动的文件(开始执行的入口)!!!!
│   ├── logo.svg
│   └── serviceWorker.js
└── yarn.lock 

参考链接

配置

修改端口号

  • 修改package.json文件
{ // ... "scripts": { "start": "set PORT=8080 && react-scripts start", // ... }, // ... }

关闭自动启动浏览器

  • 方法一:修改package.json文件
{ // ... "scripts": { "start": "set BROWSER=none&& react-scripts start", // ... }, // ... }

注意BROWSER=none&&之间不要留有空格。否则会出现异常弹窗。

none

这里是环境变量中将空格也设置在了BROWSER字段中,但是create-react-app没有做trim处理导致的。

  • 方法二:在根目录下新建.env文件
BROWSER=none

使用相对路径

  • 修改package.json文件,添加"homepage": "."
{ // ... "homepage": "." }

配置多环境

  • 安装dotenv-clinpm i dotenv-cli --save-dev
  • 在根目录下添加.env.dev文件
REACT_APP_URL_API=http://dev.com
REACT_APP_URL_UPLOAD=http://upload.dev.com
  • 在根目录下添加.env.sit文件
REACT_APP_URL_API=http://sit.com
REACT_APP_URL_UPLOAD=http://upload.sit.com
  • 在根目录下添加.env.prod文件
REACT_APP_URL_API=http://prod.com
REACT_APP_URL_UPLOAD=http://upload.prod.com
  • 修改package.json文件
{ // ... "scripts": { "start": "dotenv -e .env.dev react-scripts start", "build:sit": "dotenv -e .env.sit react-scripts build", "build:prod": "dotenv -e .env.prod react-scripts build", "test": "react-scripts test", "eject": "react-scripts eject" }, // ... }
  • index.html中使用%REACT_APP_URL_API%
  • js/jsx中:process.env.REACT_APP_URL_API

样式

CSS行内样式

  • 可以写行内样式
const styleDemo = { background: 'red' } // ... <div style={styleDemo}></div>

引入外部CSS样式

  • 写在单独的文件中(比如index.css),通过import './index.css'引入
.theList { background: lightcoral } .theList p{ background: greenyellow; }
// 备注:这种方式引入的css样式是全局的,很容易造成全局污染 import './index.css' // ... <div className="theList"> <div>theList</div> <p>backgound</p> </div>
  • 使用css module方式(css文件必须带有.module,比如index.module.css),通过import styles from './index.module.css'引入
.red { --red: red; background: var(--red); } .theList { background: lightcoral } .theList p{ background: greenyellow; } .theList .blue{ background: lightskyblue; }
// 备注:css module的方式不用全局污染,会自动生成 home_theList__1uczA 这种类名 import styles from './index.module.css' // ... <div className={styles.theList}> <div>theList</div> <p>backgound</p> <div className={styles.blue}>我们都是中国人</div> </div>

使用less

  • 弹出配置:npm run eject,执行后会在根目录下出现config/目录和scripts/目录

  • 下载lessless-loadernpm i less less-loader --save-dev

  • 修改config/webpack.config.js文件

// ... // style files regexes const cssRegex = /\.css$/; const cssModuleRegex = /\.module\.css$/; const sassRegex = /\.(scss|sass)$/; const sassModuleRegex = /\.module\.(scss|sass)$/; const lessRegex = /\.less$/; const lessModuleRegex = /\.module\.less$/; // ... module.exports = function(webpackEnv) { // ... return { // ... module: { // ... rules: [ // ... { oneOf: [ // ... // 在 sassModuleRegex 后面,必须在 file-loader 之前 { test: lessRegex, exclude: lessModuleRegex, use: getStyleLoaders({ importLoaders: 1, sourceMap: isEnvProduction && shouldUseSourceMap, }, 'less-loader'), sideEffects: true, }, { test: lessModuleRegex, use: getStyleLoaders({ importLoaders: 1, sourceMap: isEnvProduction && shouldUseSourceMap, modules: true, }, 'less-loader'), }, // ... ] } // ... ] // ... } // ... } // ... }
  • 具体使用参考上面的引入外部CSS样式

多个类名

  • 下载classnamesnpm i classnames --save

  • 在组件中使用

// ... import cx from 'classnames' // ... <span className={cx( "todo-item__text", todo && todo.completed && "todo-item__text--completed" )}> </span>

路由-Router

  • 下载:npm i react-router-dom --save

  • 如果用withRouter的话,还需要下载react-routernpm i react-router --save

  • 新建src/router/目录

  • src/router/目录下新建index.jsx

import React from 'react' import { Route, Switch } from 'react-router-dom' import HomePage from '../views/home' import ProductsPage from '../views/products' import ProductsDetailPage from '../views/products/detail' import AboutPage from '../views/about' import NotFound from '../views/error/404' function Routes() { return ( <Switch> <Route path="/" exact component={HomePage}></Route> {/* <Route path="/" exact render={() => <Redirect to="/products"></Redirect>}></Route> */} <Route path="/products" exact component={ProductsPage}></Route> <Route path="/products/:id" component={ProductsDetailPage}></Route> <Route path="/about" component={AboutPage}></Route> <Route component={NotFound}></Route> </Switch> ) } export default Routes
  • 修改src/App.js文件
import React from 'react'; import { HashRouter } from 'react-router-dom' import Routes from './router' import './App.css'; function App() { return ( <HashRouter> <Routes></Routes> </HashRouter> ) } export default App;
  • 子路由示例:修改src/views/about/index.jsx文件
import React from 'react' import { Route, Switch, Redirect } from 'react-router-dom' import LayoutDefault from '../layout' import AboutCompany from './company' import AboutHistory from './history' import AboutLocation from './location' import AboutServices from './services' function AboutPage() { return ( <LayoutDefault> <div>About page</div> <Route path='/about' exact component={AboutCompany}/> <Route path="/about/history" component={AboutHistory}></Route> <Route path="/about/services" component={AboutServices}></Route> <Route path="/about/location" component={AboutLocation}></Route> </LayoutDefault> ) } export default AboutPage

数据管理-Redux

参考:Redux笔记

路由切换动画

react-transition-group

  • 安装:npm i react-transition-group --save

  • src/components/目录下新建AnimatedSwitch/目录

  • src/components/AnimatedSwitch/目录下新建index.css

/* 很多动画都需要给路由对应组件最外层元素设置position: absolute; */ .page { /* will-change: transform; */ position: absolute; left: 0; top: 0; width: 100%; height: 100%; } /* 帧动画 */ .fade-enter { opacity: 0; } .fade-enter.fade-enter-active { opacity: 1; transition: opacity 300ms ease-in; } .fade-exit { opacity: 1; } .fade-exit.fade-exit-active { opacity: 0; transition: opacity 300ms ease-in; }
  • src/components/AnimatedSwitch/目录下新建index.jsx
import React from 'react' import { TransitionGroup, CSSTransition } from 'react-transition-group' import { Route, Switch } from 'react-router-dom' import './index.css' const AnimatedSwitch = props => { const { children } = props return ( <Route render={({ location }) => ( <TransitionGroup> <CSSTransition key={location.pathname} classNames={props.type || 'fade'} timeout={props.duration || 300} > <Switch location={location}>{children}</Switch> </CSSTransition> </TransitionGroup> )} /> ) } export default AnimatedSwitch
  • 修改src/router/index.jsx,将其中的Switch修改为AnimatedSwitch
// ... import AnimatedSwitch from '../components/AnimatedSwitch' // ... function Routes() { return ( <AnimatedSwitch> <Route path="/" exact component={HomePage}></Route> <Route path="/products" exact component={ProductsPage}></Route> <Route path="/products/:id" component={ProductsDetailPage}></Route> <Route path="/about" component={AboutPage}></Route> <Route path="/todos" component={Todos}></Route> <Route component={NotFound}></Route> </AnimatedSwitch> ) } // ...
  • 修改src/views/layout/index.jsx文件,给最外层添加className="page"

react-animated-router

  • 安装npm i react-animated-router --save

  • 修改src/router/index.jsx,将其中的Switch修改为AnimatedRouter

// ... import AnimatedRouter from 'react-animated-router' import 'react-animated-router/animate.css' // ... function Routes() { return ( <AnimatedRouter> <Route path="/" exact component={HomePage}></Route> <Route path="/products" exact component={ProductsPage}></Route> <Route path="/products/:id" component={ProductsDetailPage}></Route> <Route path="/about" component={AboutPage}></Route> <Route path="/todos" component={Todos}></Route> <Route component={NotFound}></Route> </AnimatedRouter> ) } // ...

ant-motion

  • 官方网站:Ant-Motion

  • 一个强大的动画库。能够快速在React框架中使用动画

  • 提供了单项,组合动画,以及整套解决方案

  • 安装npm i rc-queue-anim --save

  • 修改src/router/index.jsx文件

import React from 'react' import { Route, withRouter } from 'react-router-dom' import QueueAnim from 'rc-queue-anim' import HomePage from '../views/home' import ProductsPage from '../views/products' import ProductsDetailPage from '../views/products/detail' import AboutPage from '../views/about' import Todos from '../views/todos' // import NotFound from '../views/error/404' const routes = [ { path: '/', name: 'Home', Component: HomePage }, { path: '/products', name: 'Products', Component: ProductsPage }, { path: '/products/:id', name: 'ProductDetail', Component: ProductsDetailPage }, { path: '/about', name: 'About', Component: AboutPage }, { path: '/todos', name: 'Todos', Component: Todos }, ] function Routes(props) { const { pathname } = props.location const page = routes.find(v=>v.path === pathname) return ( <QueueAnim type="scaleX" duration={800}> <Route key={page.path} exact path={page.path} component={page.Component}></Route> </QueueAnim> ) } export default withRouter(Routes)

antd-mobile

按需引入

  • 下载antd-mobilenpm i antd-mobile --save

  • 按需引入需要下载babel-plugin-importnpm i babel-plugin-import --save-dev

  • 修改package.json文件

{ // ... "babel": { // ... "plugins": [ ["import", { "libraryName": "antd-mobile", "style": true }] // true: less "css": css ] }, // ... }
  • 在组件中使用
import { Button } from 'antd-mobile' <Button type="primary">primary</Button>

修改主题

  • 需要提前下载和配置less。配置的时候注意,.less文件不能开启module

  • 修改package.json文件,在这里添加要覆盖的变量。变量参考:ant-design-mobile 默认主题样式

{ // ... "theme": { "brand-primary": "red", "color-text-base": "#333", "primary-button-fill": "green" }, // ... }
  • 修改config/webpack.config.js文件
// ... const theme = require('../package.json').theme; // ... module.exports = function(webpackEnv) { // ... return { // ... module: { // ... rules: [ // ... { oneOf: [ // ... // 在 sassModuleRegex 后面,必须在 file-loader 之前 { test: lessRegex, exclude: lessModuleRegex, use: [ ...getStyleLoaders({ importLoaders: 1, sourceMap: isEnvProduction && shouldUseSourceMap, }), { loader: 'less-loader', options: { modifyVars: theme } } ], sideEffects: true, }, { test: lessModuleRegex, use: [ ...getStyleLoaders({ importLoaders: 1, sourceMap: isEnvProduction && shouldUseSourceMap, modules: true, }), { loader: 'less-loader', options: { modifyVars: theme } } ], }, // ... ] } // ... ] // ... } // ... } // ... }
  • 重启即可看到效果

使用svg

  • 下载svg-sprite-loadernpm i svg-sprite-loader --save-dev

  • src/目录下新建icons/目录

  • src/icons/目录下新建svg/目录,用来放置svg图标文件

  • src/components/目录下新建SvgIcon/目录

  • src/components/SvgIcon/目录下新建style.module.less文件

.svg-class { width: 1em; height: 1em; vertical-align: -0.15em; fill: currentColor; overflow: hidden; }
  • src/components/SvgIcon/目录下新建index.jsx文件
import React from "react"; import PropTypes from "prop-types"; import styles from "./style.module.less"; const SvgIcon = props => { const { iconClass, fill } = props; return ( <i aria-hidden="true" className="svg-icon"> <svg className={styles["svg-class"]}> <use xlinkHref={"#icon-" + iconClass} fill={fill} /> </svg> </i> ); }; SvgIcon.propTypes = { // svg名字 iconClass: PropTypes.string.isRequired, // 填充颜色 fill: PropTypes.string }; SvgIcon.defaultProps = { fill: "currentColor" }; export default SvgIcon;
  • src/icons/目录下新建index.js文件
const requireAll = requireContext => requireContext.keys().map(requireContext); const svgs = require.context("./svg", false, /\.svg$/); requireAll(svgs);
  • 修改App.js文件
// ... import "./icons" // ...
  • 修改config/webpack.config.js文件,让src/icons目录下面的svg文件使用svg-sprite-loader打包
// ... module.exports = function(webpackEnv) { // ... return { // ... module: { // ... rules: [ // ... { oneOf: [ // ... // 必须在 file-loader 之前 { test: /\.(svg)$/i, loader: 'svg-sprite-loader', include: path.resolve(__dirname, "../src/icons"), options: { // symbolId和use使用的名称对应 <use xlinkHref={"#icon-" + iconClass} /> symbolId: "icon-[name]" } }, // ... ] } // ... ] // ... } // ... } // ... }
  • 在组件中使用
import SvgIcon from './components/SvgIcon' <SvgIcon iconClass="android" />

服务器端渲染

  • 首先需要运行在服务器端。这里以koa2为例

  • 基本的服务器端代码

// server/server.js const Koa = require('koa') const cors = require('koa2-cors') const router = require('./router') const app = new Koa() // cors app.use(cors({ origin: function(ctx) { return '*' }, // exposeHeaders: [], maxAge: 5, credentials: true, allowMethods: ['GET', 'POST'], allowHeaders: ['Content-Type', 'Authorization', 'Accept'] })) // router app.use(router.routes()).use(router.allowedMethods()) app.listen(9093)
// server/router/index.js const Router = require('koa-router') const userRouter = require('./user') let router = new Router() router.use('/user', userRouter.routes()) // 所有的路径 最前面加一个 /api router.use('/api', router.routes()) module.exports = router

思路

  • node只支持commonjs规范的模块,使用babel-nodeES6 module转成commonjs模块

  • 测试jsx语法的支持情况

  • 骨架public/index.html,复制一份,删除掉其中的注释等。供后续手动拼接

  • 使用renderToStringjsx转成字符串

  • 使用css-modules-require-hook处理代码中requirecss文件

  • 使用asset-require-hook处理代码中require的图片、字体等资源文件(如果代码中使用import引入的,需要修改成require方式引入)

  • 代码打包后,会在build/目录下有asset-manifest.json,该文件记录了初始时需要加载的文件和所有的文件

  • 将初始时需要引入的cssjs文件手动引入

实现

让 node 支持 ES6 module

  • 安装@babel/cli@babel/nodenpm i @babel/cli @babel/node --save

  • 修改package.json文件的scripts部分:"server": "cross-env NODE_ENV=test nodemon --config nodemon --exec babel-node --harmony ./server/server.js"

  • 修改server/server.js等文件,改成ES6 module规范方式引入测试

// server/server.js import Koa from 'koa' import cors from 'koa2-cors' import router from './router' const app = new Koa() // cors app.use(cors({ origin: function(ctx) { return '*' }, // exposeHeaders: [], maxAge: 5, credentials: true, allowMethods: ['GET', 'POST'], allowHeaders: ['Content-Type', 'Authorization', 'Accept'] })) // router app.use(router.routes()).use(router.allowedMethods()) app.listen(9093)

测试 jsx 支持情况

  • 修改server/server.js文件
// ... import Router from 'koa-router' import React from 'react' const router = new Router() function App() { return (<h2>react jsx test</h2>) } router.get('/', ctx => { ctx.response.body = <App></App> })
  • 如果不对,需要配置.babelrc文件。比如如下配置
{ "presets": [ "react-app" ] }

改造client代码

  • src/目录下新建一个app.js文件,将客户端和服务器端公共的代码放在这里。src/index.js修改后代码类似如下
import React from 'react'; import ReactDOM from 'react-dom'; import { BrowserRouter } from 'react-router-dom' import { Provider } from 'react-redux' import store from './store' import App from './app' import * as serviceWorker from './serviceWorker'; ReactDOM.render( <Provider store={store}> <BrowserRouter> <App></App> </BrowserRouter> </Provider>, document.getElementById('root') ); // If you want your app to work offline and load faster, you can change // unregister() to register() below. Note this comes with some pitfalls. // Learn more about service workers: https://bit.ly/CRA-PWA serviceWorker.unregister();
  • src/store/index.js文件中如果有加window.devToolsExtension,需要区分下。比如可以配置process.env。修改package.json文件的scripts部分"server": "cross-env NODE_ENV=test platform=server nodemon --config nodemon --exec babel-node --harmony ./server/server.js"。然后修改src/store/index.js文件如下
import { createStore, applyMiddleware, compose } from 'redux' import rootReducer from './reducers' import thunk from 'redux-thunk' const store = process.env.platform === 'client' ? createStore(rootReducer, compose( applyMiddleware(thunk), window.devToolsExtension ? window.devToolsExtension() : f=>f )) : createStore(rootReducer, compose( applyMiddleware(thunk) )) export default store

改造server代码

  • 修改server/server.js文件,添加静态文件支持
// ... import koaStatic from 'koa-static' // ... // static 放在router之前 const staticPath = '../build' app.use(koaStatic( path.join(__dirname, staticPath) )) // router app.use(router.routes()).use(router.allowedMethods()) // ...
  • 修改server/server.js文件,添加非静态文件和接口的处理
// ... import templateController from './template' // ... app.use(templateController) // static const staticPath = '../build' app.use(koaStatic( path.join(__dirname, staticPath) )) // router app.use(router.routes()).use(router.allowedMethods()) // ...
  • 添加server/template.js
export default (ctx, next) => { if (ctx.url.startsWith('/api/') || ctx.url.startsWith('/static/')) { return next() } ctx.response.body = 'test' }
  • 修改server/template.js文件,添加renderToString编译jsx代码
import React from 'react'; // 服务端需要使用 StaticRouter import { StaticRouter } from 'react-router-dom' import { Provider } from 'react-redux' import store from '../src/store' import App from '../src/app' import { renderToString } from 'react-dom/server' // 服务器端使用 renderToString 解析对应的jsx,生成html模板 function template(url) { let context = {} const markup = renderToString( (<Provider store={store}> <StaticRouter location={url} context={context} > <App></App> </StaticRouter> </Provider>) ) return markup } export default (ctx, next) => { if (ctx.url.startsWith('/api/') || ctx.url.startsWith('/static/')) { return next() } ctx.response.body = template(ctx.url) }
  • 修改server/template.js文件,添加hooks处理引入的css文件、图片和字体等资源文件
// hooks - 放在文件最上面 import csshook from 'css-modules-require-hook/preset' import assethook from 'asset-require-hook' assethook({ extensions: ['png'] }) // ...
  • 跟目录下新建cmrh.conf.js文件
module.exports = { // Same scope name as in webpack build generateScopedName: '[name]__[local]___[hash:base64:5]', }
  • 修改server/template.js文件,将骨架和解析后的代码合并
// ... export default (ctx, next) => { if (ctx.url.startsWith('/api/') || ctx.url.startsWith('/static/')) { return next() } const page = `<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="description" content="Web site created using create-react-app" /> <title>React App</title> </head> <body> <noscript>You need to enable JavaScript to run this app.</noscript> <div id="root">${template(ctx.url)}</div> </body> </html> ` ctx.response.body = page }
  • 修改server/template.js文件,引入初始的cssjs资源
// ... import { entrypoints } from '../build/asset-manifest.json' // ... function getFiles(arr) { const obj = {} arr.forEach(file => { const arr = file.split('.') const ext = arr[arr.length - 1] const types = ['css', 'js'] if (types.indexOf(ext) !== -1) { obj[ext] ? obj[ext].push(file) : (obj[ext] = [file]) } }) return obj } // 将需要引入的css,组成 link 字符串 function cssLinks(arr) { let res = `` arr.forEach(item => { res += `<link rel="stylesheet" href="${item}">` }) return res } // 将需要引入的css,组成 script 字符串 function jsScripts(arr) { let res = `` arr.forEach(item => { res += `<script src="${item}"></script>` }) return res } export default (ctx, next) => { if (ctx.url.startsWith('/api/') || ctx.url.startsWith('/static/')) { return next() } // const page = fs.readFileSync('./build/index.html', 'utf-8') const fileObj = getFiles(entrypoints) const page = `<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="description" content="Web site created using create-react-app" /> <title>React App</title> ${cssLinks(fileObj['css'])} </head> <body> <noscript>You need to enable JavaScript to run this app.</noscript> <div id="root">${template(ctx.url)}</div> ${jsScripts(fileObj['js'])} </body> </html> ` ctx.response.body = page }
  • server/template.js文件完整代码
// hooks // css hook,处理通过 require 引入的 css // 需要在根目录下新建并配置 cmrh.conf.js 文件 import csshook from 'css-modules-require-hook/preset' // asset hook 处理图片、字体等资源文件 import assethook from 'asset-require-hook' assethook({ extensions: ['png'] }) import React from 'react'; // 服务端需要使用 StaticRouter import { StaticRouter } from 'react-router-dom' import { Provider } from 'react-redux' import store from '../src/store' import App from '../src/app' import { renderToString } from 'react-dom/server' import { entrypoints } from '../build/asset-manifest.json' // 服务器端使用 renderToString 解析对应的jsx,生成html模板 function template(url) { let context = {} const markup = renderToString( (<Provider store={store}> <StaticRouter location={url} context={context} > <App></App> </StaticRouter> </Provider>) ) return markup } // css、js需要手动插入 // build/asset-manifest.json 文件的 entrypoints 就是初始化时需要加载的css、js文件 // 该方法将css、js区分开,放在不同的数组内。方便后续处理 function getFiles(arr) { const obj = {} arr.forEach(file => { const arr = file.split('.') const ext = arr[arr.length - 1] const types = ['css', 'js'] if (types.indexOf(ext) !== -1) { obj[ext] ? obj[ext].push(file) : (obj[ext] = [file]) } }) return obj } // 将需要引入的css,组成 link 字符串 function cssLinks(arr) { let res = `` arr.forEach(item => { res += `<link rel="stylesheet" href="${item}">` }) return res } // 将需要引入的css,组成 script 字符串 function jsScripts(arr) { let res = `` arr.forEach(item => { res += `<script src="${item}"></script>` }) return res } export default (ctx, next) => { if (ctx.url.startsWith('/api/') || ctx.url.startsWith('/static/')) { return next() } // const page = fs.readFileSync('./build/index.html', 'utf-8') const fileObj = getFiles(entrypoints) const page = `<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="description" content="Web site created using create-react-app" /> <title>React App</title> ${cssLinks(fileObj['css'])} </head> <body> <noscript>You need to enable JavaScript to run this app.</noscript> <div id="root">${template(ctx.url)}</div> ${jsScripts(fileObj['js'])} </body> </html> ` ctx.response.body = page }

创作不易,若本文对你有帮助,欢迎打赏支持作者!

 分享给好友: