1.初始化项目
yarn init
生成一个最基础的package.json文件
{
"name": "react-scaffold",
"version": "1.0.0",
"description": "React脚手架",
"main": "index.js",
"license": "MIT"
}
2.依赖安装
yarn add react --save // react框架安装
yarn add react-dom --save // react-dom安装
yarn add react-router-dom --save // react-router-dom安装
yarn add less --dev // less包
yarn add express // express服务
yarn add webpack --dev // 打包工具webpack
yarn add webpack-cli --dev // webpack cli工具
yarn add webpack-merge --dev // 合并webpack配置文件工具
yarn add html-webpack-plugin --dev // 自动生成打包后的html文件
yarn add css-loader style-loader less-loader --dev // css解析loader
yarn add url-loader file-loader --dev // 图片处理loader
yarn add webpack-dev-middleware --dev // 自动打包
yarn add webpack-hot-middleware --dev // 热更新
yarn add uglifyjs-webpack-plugin --dev // 压缩文件
yarn add optimize-css-assets-webpack-plugin --dev // 压缩css文件
yarn add babel-loader --dev // webpack中需要用到的loader.
yarn add @babel/core --dev // 用于解析最新ES标准
yarn add @babel/polyfill --dev // 用于解析最新ES语法api
yarn add @babel/plugin-transform-runtime --dev //
yarn add @babel/runtime --dev //babel transform runtime 插件的依赖.
yarn add @babel/preset-env --dev // 根据预设的环境自动选用合适babel插件
yarn add @babel/preset-es2015 --dev // 用于转化 ES6
yarn add @babel/preset-react --dev // 用于解析 JSX
3.babel配置
Babel将最新标准的javaScript
语法向下编译至ES5版本
在根目录下新建文件babelrc
{
"presets": [
["@babel/env", {
// 指定需要兼容的浏览器类型和版本
"targets": {
"browsers": ["last 2 versions"]
}
}],
"@babel/react" // 解析jsx
]
}
4.eslint配置
yarn add eslint babel-eslint eslint-plugin-babel eslint-config-prettier eslint-plugin-react --dev // eslint工具
yarn add prettier eslint-plugin-prettier --dev // 设置代码风格插件
新建eslintrc
,配置eslint规则
也可以新建.eslintignore
文件,配置eslint忽略的文件
{
"env": {
"es6": true,
"browser": true,
"node": true
},
"parser": "babel-eslint",
"extends": [
"prettier", // 代码风格统一工具
"plugin:react/recommended", // 支持react
],
"plugins": [
"babel",
"react",
"prettier"
],
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "module",
"ecmaFeatures": {
"jsx": true
}
},
"rules": {
"prettier/prettier": "error" // // 被prettier标记的地方抛出错误信息
}
}
新建.prettierrc.js
,设置代码风格规则
module.exports = {
printWidth: 80, //一行的字符数,如果超过会进行换行,默认为80
tabWidth: 2, //一个tab代表几个空格数,默认为80
useTabs: false, //是否使用tab进行缩进,默认为false,表示用空格进行缩减
singleQuote: false, //字符串是否使用单引号,默认为false,使用双引号
semi: true, //行位是否使用分号,默认为true
trailingComma: 'all', //是否使用尾逗号,有三个可选值"<none|es5|all>"
bracketSpacing: true, //对象大括号直接是否有空格,默认为true,效果:{ foo: bar }
}
5.webpack
在package.json中配置脚本命令
通过不同的命令执行开发或生产环境的打包
"scripts": {
"build:dev": "webpack --config ./webpack.dev.js",
"build:prod": "webpack --config ./webpack.prod.js"
},
5.1webpack公用配置
根目录下新建webpack-common-js文件
const path = require("path")
const webpack = require('webpack')
const HtmlwebpackPlugin = require('html-webpack-plugin')
// __dirname是node中的一个全局变量,它指向当前执行脚本所在的目录
const ROOT_PATH = path.resolve(__dirname)
const HTML_PATH = path.resolve(ROOT_PATH, 'src/index.html')
const BUILD_PATH = path.resolve(ROOT_PATH, 'dist')
module.exports = {
output: {
path: BUILD_PATH, // bundle输出目录
chunkFilename: "bundle.[hash].js", // bundle生成文件的名称
publicPath: '/',
},
resolve: {
extensions: [".ts", ".tsx", ".js", ".jsx", ".json"], // // 自动补全后缀
alias: {
'@': path.resolve('src'), // 相对路径配置
},
},
module: {
rules: [
// 将js或者jsx结尾的文件,使用babel解析
{
test: /\.(js|jsx)?$/,
exclude: /node_modules/,
use: ['babel-loader'],
},
// css解析loader
{
test: /\.(less|css)$/,
include: [/src/],
use: [
require.resolve('style-loader'),
{
loader: require.resolve('css-loader'),
},
{
loader: require.resolve('less-loader'),
},
],
},
// 图片处理loader
{
test: /\.(png|jpg|gif)$/,
use: [{
loader: 'url-loader',
options: {
limit: 8192 // 小于等于8K的图片会被转成base64编码,直接插入HTML中,减少HTTP请求
}
}]
},
]
},
// 插件
plugins: [
//在build目录下自动生成index.html
new HtmlwebpackPlugin({
template: HTML_PATH,
filename: 'index.html',
}),
]
}
5.2webpack开发环境配置
const merge = require('webpack-merge');
const path = require("path")
const webpack = require('webpack')
const common = require('./webpack.common.js');
const APP_PATH = path.resolve(__dirname, 'src/index.jsx')
module.exports = merge(common, {
mode: 'development', // 开发环境模式
// 入口地址
entry: [
"webpack-hot-middleware/client?path=/__webpack_hmr",
"@babel/polyfill",
APP_PATH
],
//启用source-map方便调试,将错误消息位置映射到模块
devtool: 'source-map',
plugins: [
new webpack.HotModuleReplacementPlugin(), // 热更新插件
]
})
5.2.1自动打包与热更新配置
webpack 可以通过watch mode
方式来监听资源变化来进行自动打包操作,但是每次打包后的结果将会存储到本地硬盘中,而 IO 操作是非常耗资源时间的,无法满足本地开发调试需求。
而webpack-dev-middleware
拥有以下几点特性:
以 watch mode 启动 webpack ,监听的资源一旦发生变更,便会自动编译,生产最新的 bundle
在编译期间,停止提供旧版的 bundle 并且将请求延迟到最新的编译结果完成之后
webpack 编译后的资源会存储在内存中,当用户请求资源时,直接于内存中查找对应资源,减少去硬盘中查找的 IO 操作耗时
新建server.js文件
通过启动一个 express
服务,将 webpackDevMiddleware()的结果通过 app.use 方法注册为 express 服务的中间函数。webpackDevMiddleware()的执行结果返回的是一个 express 的中间件。它作为一个容器,将 webpack 编译后的文件存储到内存中,然后在用户访问 express 服务时,将内存中对应的资源输出返回。
const path = require("path")
const express = require("express")
const webpack = require("webpack")
const webpackDevMiddleware = require("webpack-dev-middleware")
const webpackConfig = require('./webpack.config.js')
const app = express()
const DIST_DIR = path.join(__dirname, "dist") // 设置静态访问文件路径
const complier = webpack(webpackConfig) // webpack options
// webpackDevMiddleware options
const devMiddlewareOptions = {
publicPath: webpackConfig.output.publicPath, //绑定中间件的公共路径,与webpack配置的路径相同
hot: true, // 热更新
}
app.use(webpackDevMiddleware(complier, devMiddlewareOptions))
app.use(express.static(DIST_DIR))
app.listen(8000, function(){
console.log("成功启动:localhost:"+ 8000)
})
然后在package.json中添加
"scripts": {
"start": "node server.js"
},
webpack-hot-middleware
中间件将其本身安装为Webpack插件,并监听编译器事件。
每个连接的客户端都会获得Server Sent Events连接,服务器将向编译器事件的连接客户端发送通知。
当客户端收到消息时,它将检查本地代码是否是最新的。如果不是最新的,它将触发webpack热模块重新加载。
// server.js
const webpackHotMiddleware = require('webpack-hot-middleware')
app.use(webpackHotMiddleware(complier))
// webpack.config.js
plugins: [
new webpack.HotModuleReplacementPlugin()
]
// 项目入口处 src/index.jsx
if (module.hot) {
module.hot.accept();
}
配置完webpack-dev-middleware
和 webpack-hot-middleware
后,webpack就能够自动打包,热更新页面了。
5.3webpack生产环境配置
const merge = require('webpack-merge')
const path = require("path")
const UglifyJSPlugin = require('uglifyjs-webpack-plugin')
const common = require('./webpack.common.js')
module.exports = merge(common, {
mode: 'production', // 生产环境模式
entry: {
main: path.resolve(__dirname, "src/index.jsx")
},
devtool: 'inline-source-map',
optimization: {
splitChunks: {
chunks: 'all' // 第三方库分离
},
minimizer: [
new UglifyJSPlugin({
uglifyOptions: {
compress: {
unused: true,
dead_code: true,
drop_console: true,
drop_debugger: true,
pure_funcs: ['console.log']
},
},
sourceMap: true,
}),
],
},
performance: {
hints: "warning", // 抛出提示信息类型
maxAssetSize: 30000000, // 入口起点的最大体积
maxEntrypointSize: 50000000, // 生成文件的最大体积
assetFilter: function(assetFilename) {
// 只给出 js、css 文件的性能提示
return assetFilename.endsWith('.css') || assetFilename.endsWith('.js');
},
},
plugins: []
})
5.3.1代码压缩
const UglifyJSPlugin = require('uglifyjs-webpack-plugin')
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')
module.exports = {
...
optimization: {
minimizer: [
new UglifyJSPlugin({
uglifyOptions: {
compress: {
unused: true, // 删除未引用的函数和变量
dead_code: true, // 删除无法访问的代码
drop_console: true, // 放弃对console.*函数的调用
drop_debugger: true, // 删除debugger语句
pure_funcs: ['console.log'] // 移除console
},
},
}),
new OptimizeCSSAssetsPlugin({}), // css压缩
],
},
}
6. React
6.1React解析
浏览器不能解析React中的JSX语法,需要安装babel来解析:
yarn add babel-loader --dev
再安装用于解析es6和jsx语法的插件:
yarn add @babel/preset-env @babel/preset-es2015 @babel/preset-react --dev
以及React框架
yarn add react react-dom --save
还需在.babelrc进行相关配置
{
"presets": [
["@babel/env", {
"targets": {
"browsers": ["last 2 versions"]
}
}],
"@babel/react"
]
}
6.2React-router+按需加载
安装yarn add react-router-dom --save
依赖
按需加载,也就是代码拆分,通过路由来判断何时来加载对应模块代码,缩短首页渲染时间
import React, { lazy, Suspense } from "react"
import { BrowserRouter, Switch, Route } from 'react-router-dom'
const Component1 = lazy(() => import(/* webpackChunkName: "Component1" */ '../components/Component1'))
const Component2 = lazy(() => import(/* webpackChunkName: "Component2" */ '../components/Component2'))
function App() {
return (
<Suspense fallback={<div>Loading</div>}>
<BrowserRouter>
<Switch>
<Route path="/component2" component={Component2}/>
<Route path="/" component={Component1}/>
</Switch>
</BrowserRouter>
</Suspense>
)
}
export default App
然后在webpack配置中加上chunkFilename
output: {
chunkFilename: "[name].[hash].js", // bundle生成文件的名称
},
这样打包就能够生存单独的模块代码,实现按需加载
6.3React局部更新
前面已经实现了webpack热更新,但是在React中,state也会被重新刷新,如果想保存state,需要一些插件的帮助
安装
yarn add react-hot-loader @hot-loader/react-dom --dev
-
配置babelrc
{ ... "plugins": ["react-hot-loader/babel"] }
-
更新
webpack.dev.js
resolve: { alias: { 'react-dom': '@hot-loader/react-dom' } },
-
在组件中引入
react-hot-loader
import { hot } from 'react-hot-loader/root' ... export default hot(App)
6.4React-reudx基本配置
安装yarn add redux react-redux --save
store
配置
在src/controller
下创建store.js
文件
import { createStore } from 'redux'
import reducer from '.reducer'
const store = createStore(reducer)
export default store
reducer
配置
在src/controller
下创建reducer.js
文件
reducer已拆分,需引入各个模块下的reducer
import { combineReducers } from 'redux'
import { reducer as Component1Reducer } from '../../components/Component1/controller'
const reducer = combineReducers({
component1: Component1Reducer,
})
export default reducer
然后在项目入口处用Provider
将所有组件包裹起来
import { Provider } from 'react-redux'
import store from '../controller/store' // 总的store
<Provider store={store}>
...
</Provider>
然后在各个模块下建立独立的reducer
// types
export const types = {
TITLE_CHANGE: 'TITLE_CHANGE',
}
// actionTypes
import { types } from './types'
export function titleChange() {
return {type: types.TITLE_CHANGE}
}
// reducer
import { types } from './types'
const defaultState = {
title: 'hello',
}
export default (state = defaultState, action) => {
switch (action.type) {
case types.TITLE_CHANGE:
const title = action.payload
return { ...state, title }
default:
return state
}
}
// index.js 将action,reduecer等导出
import reducer from './reducer'
import { types as component1Types } from './types'
import { actionTypes as component1ActionTypes } from './actionTypes'
export { reducer, component1Types, component1ActionTypes }
6.5reducer动态添加
在Redux中,reducer 决定了最后的整个状态的数据结构,在 store 中有一个 replaceReducer(nextReducer)
方法,它是 Redux 中的一个高阶 API ,该函数接收一个 nextReducer
参数,用于替换 store 中原有的 reducer ,以此可以改变 store 中原有的状态的数据结构
在src/controller/reducer
中
import store from '../store'
const allReducer = (asyncReducers) => {
return combineReducers({
...asyncReducers,
})
}
// 在 store 中动态注入 reducer
const injectReducer = (key, reducer) => {
// 判断当前store中的asyncReducers是否存在该reducer
if (Object.hasOwnProperty.call(store.asyncReducers, key)) return
// 添加新增的reducer
store.asyncReducers[key] = reducer
// 替换原有的reducer
store.replaceReducer(allReducer(store.asyncReducers))
}
然后在src/controller/store
中
const store = () => {
// 传入的reducer是动态生成的
const store = createStore(allReducer())
// 增加了一个asyncReducer的属性对象,用来缓存旧的reducers,然后与新的reducer合并
store.asyncReducers = {}
return store
}
最后就在模块处调用injectReducer()
处传入相应的key,和reducer就可以了
警告信息:redux.js:319 Store does not have a valid reducer. Make sure the argument passed to combineReducers is an object whose values are reducers.
出现该警告信息的时候,可能是combineReducers
中的reducer值为空
可以设置一个初始reducer
// 初始state
const initialState = {}
// action对象
const ACTION_HANDLERS = {}
const rootReducer = (state = initialState, action) => {
const handler = ACTION_HANDLERS[action.type]
return handler ? handler(state, action) : state
}
const allReducer = (asyncReducers = {}) => {
return combineReducers({
...asyncReducers,
rootReducer,
})
}
6.6Redux相关库/插件
6.6.1redux-actions
当项目较大时,action就会有很多,reducer中也就需要些大量的swich来对action.type进行判断。
redux-actions
当出现就是了简化actionCreator,和reducer中的操作
-
createAction
// 创建action import { createAction } from 'redux-actions' const titleChange = createAction(types.TITLE_CHANGE)
-
handleActions
// 处理action的操作,返回一个reduce import { handleActions } from 'redux-actions' import { types } from './types' const state = { title: 'hello' } const reducer = { [types.TITLE_CHANGE]: (state, action) => { const title = action.payload return { ...state, title } }, } handleActions(reducer, state)
6.6.2reselect
react-redux自身提供了connect函数,可以通过mapStateToProps获取state,但是当我们需要计算一些state时候,每计算一次就会去调用一次mapStateToProps。
因此,为了减少渲染压力,就产生了reselect,reselect最重要的就是运用了缓存机制。
- selector可以计算衍生的数据,让redux做到存储尽可能少的state。
- 并且只有在参数变化的时候才计算.
- selector是可以组合的,他们可以作为输入,传递到其他的selector.
// controller/selector 创建Selector
import { createSelector } from 'reselect'
const component1Selector = (state) => {
return state.component1
}
const title = createSelector(component1Selector, (state) => state.title)
export const selector = { title }
// controller/index
import { selector as component1Selector } from './selector'
export { component1Selector }
// 引用Selector
import { component1Selector, component1ActionTypes } from './controller'
const title = useSelector(component1Selector.title)
6.6.3redux-devtools-extension
redux调试工具
首先通过Chrome拓展程序安装Redux DevTools工具
在store.js
中
const is_DEV = process.env.NODE_ENV === 'development'
const store = createStore(
allReducer(),
is_DEV &&
window.__REDUX_DEVTOOLS_EXTENSION__ &&
window.__REDUX_DEVTOOLS_EXTENSION__(),
)
配置完成就可以在Chrome控制台中查看Redux信息
6.6.3redux-observable
redux-observable,是redux的一个中间件库。redux-observable将触发的 action 转化为一个数据流,并且订阅它。可以在这些流上监听新值,可以对这些值做出反应,最后将这些流转成 action
安装yarn add rxjs redux-observable --save
首先在store中加载redux-observable中间件
import { applyMiddleware, createStore, compose } from 'redux'
import { createEpicMiddleware } from 'redux-observable'
const store = () => {
...
const epicMiddleware = createEpicMiddleware()
const middleware = [epicMiddleware]
const enhancers = []
if (is_DEV && window.__REDUX_DEVTOOLS_EXTENSION__) {
enhancers.push(window.__REDUX_DEVTOOLS_EXTENSION__())
}
const store = createStore(
allReducer(),
compose(applyMiddleware(...middleware), ...enhancers),
)
...
}
配置rootEpics
在controller/middleware
新建rootEpics.js
import { combineEpics } from 'redux-observable'
import { BehaviorSubject } from 'rxjs'
import { mergeMap } from 'rxjs/operators'
const epic$ = new BehaviorSubject(combineEpics())
export const rootEpics = (action$, state$) =>
epic$.pipe(mergeMap((epic) => epic(action$, state$)))
// 动态生成Epics
export const injectEpics = (key, newEpics) => {
epic$.next(newEpics)
}
在store.js
中添加
import { rootEpics } from './middleware'
const store = ..
// store后插入
epicMiddleware.run(rootEpics)
最后在各个模块下的epics文件中处理action
import { injectEpics } from '@/controller/middleware/rootEpics'
import { combineEpics, ofType } from 'redux-observable'
import { actionTypes } from './actionTypes'
import { map } from 'rxjs/operators'
const titleChange = (action$) => {
return action$.pipe(
ofType(actionTypes.titleChange),
map((action) => {
return actionTypes.titleChangeSucc(action.payload)
}),
)
}
injectEpics('component1_epic', combineEpics(titleChange))