React 项目搭建
一、创建项目
bash
yarn global add create-react-app
yarn create react-app demo
yarn create react-app demo --template typescriptyarn global add create-react-app
yarn create react-app demo
yarn create react-app demo --template typescript二、目录结构
src/
assets/ — 静态资源
common/ — 公共工具
components/ — 公共组件
router/ — 路由配置
store/ — 状态管理src/
assets/ — 静态资源
common/ — 公共工具
components/ — 公共组件
router/ — 路由配置
store/ — 状态管理三、配置 Webpack
安装
bash
yarn add react-app-rewired customize-cra -Dyarn add react-app-rewired customize-cra -Dpackage.json
json
"scripts": {
"start": "react-app-rewired start",
"build": "react-app-rewired build",
"test": "react-app-rewired test",
"eject": "react-scripts eject"
}"scripts": {
"start": "react-app-rewired start",
"build": "react-app-rewired build",
"test": "react-app-rewired test",
"eject": "react-scripts eject"
}config-overrides.js
jsx
const { override, addLessLoader, fixBabelImports, addWebpackAlias } = require('customize-cra')
const path = require('path')
module.exports = override(
fixBabelImports('import', { libraryName: 'antd-mobile', style: 'css' }),
addLessLoader(),
addWebpackAlias({
'@': path.resolve(__dirname, 'src'),
components: path.resolve(__dirname, 'src/components')
})
)const { override, addLessLoader, fixBabelImports, addWebpackAlias } = require('customize-cra')
const path = require('path')
module.exports = override(
fixBabelImports('import', { libraryName: 'antd-mobile', style: 'css' }),
addLessLoader(),
addWebpackAlias({
'@': path.resolve(__dirname, 'src'),
components: path.resolve(__dirname, 'src/components')
})
)跨域代理
jsx
const devServerConfig = () => config => ({
...config,
compress: true,
proxy: {
'/api': {
target: 'https://api.example.com',
changeOrigin: true
}
}
})
module.exports = {
webpack: override(/* ... */),
devServer: overrideDevServer(devServerConfig())
}const devServerConfig = () => config => ({
...config,
compress: true,
proxy: {
'/api': {
target: 'https://api.example.com',
changeOrigin: true
}
}
})
module.exports = {
webpack: override(/* ... */),
devServer: overrideDevServer(devServerConfig())
}打包配置
jsx
const addCustomize = () => config => {
if (process.env. NODE_ENV === 'production') {
config.devtool = false
config.output.path = __dirname + '../dist/demo'
config.output.publicPath = './demo'
config.plugins.push(
new CompressionWebpackPlugin({
test: /\.js$|\.css$/,
threshold: 1024
})
)
}
return config
}const addCustomize = () => config => {
if (process.env. NODE_ENV === 'production') {
config.devtool = false
config.output.path = __dirname + '../dist/demo'
config.output.publicPath = './demo'
config.plugins.push(
new CompressionWebpackPlugin({
test: /\.js$|\.css$/,
threshold: 1024
})
)
}
return config
}生产环境去除 console
jsx
const dropConsole = () => config => {
if (config.optimization.minimizer) {
config.optimization.minimizer.forEach(minimizer => {
if (minimizer.constructor.name === 'TerserPlugin') {
minimizer.options.terserOptions.compress.drop_console = true
}
})
}
return config
}const dropConsole = () => config => {
if (config.optimization.minimizer) {
config.optimization.minimizer.forEach(minimizer => {
if (minimizer.constructor.name === 'TerserPlugin') {
minimizer.options.terserOptions.compress.drop_console = true
}
})
}
return config
}四、修改端口号
bash
# 方法一:启动时设置
"start": "set PORT=9000 && react-scripts start"# 方法一:启动时设置
"start": "set PORT=9000 && react-scripts start"五、引入图片
jsx
import IMG from '../assets/logo.png'
// 或通过 require
state = { img: require('../assets/logo.png') }
<img src={require('@/assets/logo.png')} />import IMG from '../assets/logo.png'
// 或通过 require
state = { img: require('../assets/logo.png') }
<img src={require('@/assets/logo.png')} />六、Stylus 支持
bash
npm run eject
yarn add stylus stylus-loadernpm run eject
yarn add stylus stylus-loader修改 config/webpack.config.js:
jsx
oneOf: [
{
test: /\.styl$/,
loaders: ['style-loader', 'css-loader', 'stylus-loader']
}
]oneOf: [
{
test: /\.styl$/,
loaders: ['style-loader', 'css-loader', 'stylus-loader']
}
]七、装饰器支持
bash
npm i @babel/plugin-proposal-decoratorsnpm i @babel/plugin-proposal-decoratorsjsx
const { override, addDecoratorsLegacy } = require('customize-cra')
module.exports = override(addDecoratorsLegacy())const { override, addDecoratorsLegacy } = require('customize-cra')
module.exports = override(addDecoratorsLegacy())八、px 转 rem
bash
npm i postcss-pxtorem -Dnpm i postcss-pxtorem -Djsx
addPostcssPlugins([require('postcss-pxtorem')({
rootValue: 75,
propList: ['*'],
minPixelValue: 2,
selectorBlackList: ['am-']
})])addPostcssPlugins([require('postcss-pxtorem')({
rootValue: 75,
propList: ['*'],
minPixelValue: 2,
selectorBlackList: ['am-']
})])九、代码规范
Prettier
bash
npm install --save-dev --save-exact prettier
echo {}> .prettierrc.jsonnpm install --save-dev --save-exact prettier
echo {}> .prettierrc.jsonESLint
bash
npx eslint --initnpx eslint --initStylelint
bash
npm install --save-dev stylelint stylelint-config-standard stylelint-config-prettiernpm install --save-dev stylelint stylelint-config-standard stylelint-config-prettierCommitlint
bash
npm install --save-dev @commitlint/config-conventional @commitlint/cli
echo "module.exports = {extends: ['@commitlint/config-conventional']}" > commitlint.config.jsnpm install --save-dev @commitlint/config-conventional @commitlint/cli
echo "module.exports = {extends: ['@commitlint/config-conventional']}" > commitlint.config.jslint-staged
json
"lint-staged": {
"**/*.{ts,tsx}": "eslint . --cache --fix",
"**/*.{css,less}": "stylelint --fix",
"**/*": "prettier --write --ignore-unknown"
}"lint-staged": {
"**/*.{ts,tsx}": "eslint . --cache --fix",
"**/*.{css,less}": "stylelint --fix",
"**/*": "prettier --write --ignore-unknown"
}Husky
bash
pnpm add husky -D
pnpm husky
npx husky add .husky/commit-msg
npx husky add .husky/pre-commitpnpm add husky -D
pnpm husky
npx husky add .husky/commit-msg
npx husky add .husky/pre-commit十、搭建 Redux + 持久化
bash
npm i redux react-redux redux-persist redux-devtools-extension redux-thunknpm i redux react-redux redux-persist redux-devtools-extension redux-thunkjsx
import { applyMiddleware, createStore } from 'redux'
import thunk from 'redux-thunk'
import { composeWithDevTools } from 'redux-devtools-extension'
import { persistReducer, persistStore } from 'redux-persist'
import storage from 'redux-persist/lib/storage'
const persistConfig = { key: 'root', storage }
const persistedReducer = persistReducer(persistConfig, rootReducer)
export const store = createStore(persistedReducer, composeWithDevTools(applyMiddleware(thunk)))
export const persistor = persistStore(store)import { applyMiddleware, createStore } from 'redux'
import thunk from 'redux-thunk'
import { composeWithDevTools } from 'redux-devtools-extension'
import { persistReducer, persistStore } from 'redux-persist'
import storage from 'redux-persist/lib/storage'
const persistConfig = { key: 'root', storage }
const persistedReducer = persistReducer(persistConfig, rootReducer)
export const store = createStore(persistedReducer, composeWithDevTools(applyMiddleware(thunk)))
export const persistor = persistStore(store)十一、Hooks 版小型 Redux
context/reducer.js
js
export function reducer(state, { type, ...payloads }) {
switch (type) {
case 'SET_VALUE':
return { ...state, [payloads.key]: payloads.value }
case 'DELETE_VALUE': {
const curState = { ...state }
delete curState[payloads.key]
return curState
}
default:
return state
}
}export function reducer(state, { type, ...payloads }) {
switch (type) {
case 'SET_VALUE':
return { ...state, [payloads.key]: payloads.value }
case 'DELETE_VALUE': {
const curState = { ...state }
delete curState[payloads.key]
return curState
}
default:
return state
}
}context/index.js
jsx
import React, { useReducer } from 'react'
import reducer from './reducer'
const initState = {}
const Context = React.createContext(undefined)
export function usePageContext() {
const ctx = React.useContext(Context)
if (!ctx) throw new Error('必须在 Provider 中使用')
return ctx
}
export function PageContext({ children }) {
const [state, dispatch] = useReducer(reducer, initState)
const setValue = (key, value) => dispatch({ type: 'SET_VALUE', key, value })
const delValue = (key) => dispatch({ type: 'DELETE_VALUE', key })
return (
<Context.Provider value={{ state, dispatch, setValue, delValue }}>
{children}
</Context.Provider>
)
}import React, { useReducer } from 'react'
import reducer from './reducer'
const initState = {}
const Context = React.createContext(undefined)
export function usePageContext() {
const ctx = React.useContext(Context)
if (!ctx) throw new Error('必须在 Provider 中使用')
return ctx
}
export function PageContext({ children }) {
const [state, dispatch] = useReducer(reducer, initState)
const setValue = (key, value) => dispatch({ type: 'SET_VALUE', key, value })
const delValue = (key) => dispatch({ type: 'DELETE_VALUE', key })
return (
<Context.Provider value={{ state, dispatch, setValue, delValue }}>
{children}
</Context.Provider>
)
}十二、antd 使用技巧
防止 message 重复弹出:
jsx
state = { timer: null }
message2 = (msg, delay) => {
if (this.state.timer) return
message.error(msg)
this.state.timer = setTimeout(() => {
this.state.timer = null
}, delay)
}state = { timer: null }
message2 = (msg, delay) => {
if (this.state.timer) return
message.error(msg)
this.state.timer = setTimeout(() => {
this.state.timer = null
}, delay)
}十三、百度埋点
jsx
// utils/tracking.js
if (process.env. NODE_ENV === 'production') {
var _hmt = _hmt || []
;(function() {
var hm = document.createElement('script')
hm.src = 'https://hm.baidu.com/hm.js?xxx'
document.head.appendChild(hm)
})()
_hmt.push(['_setAutoPageview', false])
}// utils/tracking.js
if (process.env. NODE_ENV === 'production') {
var _hmt = _hmt || []
;(function() {
var hm = document.createElement('script')
hm.src = 'https://hm.baidu.com/hm.js?xxx'
document.head.appendChild(hm)
})()
_hmt.push(['_setAutoPageview', false])
}jsx
// 使用
import { trackData } from 'utils'
trackData(['_trackPageview', pathname])// 使用
import { trackData } from 'utils'
trackData(['_trackPageview', pathname])
Cao Note