-
简述:
-
配置.babelrc ❗❗❗
-
react-router ❗❗❗
-
按需加载 ❗❗❗
-
优化目录结构 ❗❗❗
-
模拟Ajax数据 ❗❗❗
-
-
// ++ 代表有增加的部分
-
// -- 代表有减少的部分
-
-
mkdir Gszs-React && cd Gszs-React
-
npm init(或者npm init -y)
/*修改*/ "scripts": { "build": "webpack --config webpack.dev.config.js" //方便测试 },
-
yarn -D add webpack webpack-clic //这里使用yarn安装,其速度非常快
-
touch webpack.dev.config.js vim webpack.dev.config.js
const path = require('path') module.exports = { entry : path.join(__dirname, 'src/index.js'), mode: "development", output : { path : path.join(__dirname, './dist'), filename: 'bundle.js' }, }
-
mkdir src && cd src && vim index.js
-
yarn build
-
yarn -D add @babel/core yarn -D add @babel/preset-env yarn -D add @babel/preset-react yarn -D add babel-loader
//在.babelrc中进行如下设置 { "presets": [ "@babel/preset-env", "@babel/preset-react" ], "plugins": [] }
-
yarn -D add react react-dom
-
import React ,{ComPonent} from 'react' import ReactDOM from 'react-dom' ReactDOM.render( <h3>Hello world</h3>, document.querySelector('#app') )
-
mkdir view && cd view && mkdir layout && vim HeaderMenua
// 在HeaderMenu.js里添加如下内容 import React ,{Component} from 'react' export default class HeaderMenu extends Component{ render(){ return( <div> <h3>HeaderMenu</h3> </div> ) } } //然后在index.js中将这个组件倒入,终端yarn build打包测试
-
yarn -D add react-router-dom 新建router文件夹和组件 -> mkdir router && touch router/router.js
// 创建基本的router.js, 包含两个home 跟 pagel // Home.js import React, {Component} from 'react' export default class Home extends Component{ render(){ return( <div> <h3>Home</h3> </div> ) } } // Page.js import React, {Component} from 'react' export default class Page extends Component{ render(){ return( <div> <h3>Page</h3> </div> ) } } // 在router.js中导入这两个路由组件 import React from 'react' import {BrowserRouter as Router, Route, Switch, Link} from 'react-router-dom' import Home from '../pages/Home' import Page1 from '../pages/Page' const getRouter = () => ( <Router> <div> <ul> <li><Link to='/'>首页</Link></li> <li><Link to='/page1'>Page1</Link></li> </ul> <Switch> <Route exact path='/' component={Home}/> <Route path='/page1' component={Page1}/> </Switch> </div> </Router> ) export default getRouter /* 修改index.js导入router*/
-
yarn global add webpack-dev-server
//修改 package.json "scripts": { "build": "webpack --config webpack.dev.config.js", "dev": "webpack-dev-server --config webpack.dev.config.js" //增加一个dev }, // 修改webpack.dev.config.js,增加如下这段 devServer: { // 热更 contentBase : path.join(__dirname, './dist'), hot: true }, // 终端yarn dev进行测试,访问8080
-
yarn -D add react-hot-loader
// 修改.babelrc增加如下 { "presets": [ "@babel/preset-env", "@babel/preset-react" ], "plugins": [ "react-hot-loader/babel" ] } // 在webpack.dev.config.js中的entry修改为如下 entry : [ 'react-hot-loader/patch', // 热更替 path.join(__dirname, 'src/index.js') ], //修改src/index.js import React ,{ComPonent} from 'react' import ReactDOM from 'react-dom' import getRouter from './router/router' import {AppContainer} from 'react-hot-loader' //初始化 renderWithHotReload(getRouter()) /*热更新*/ if (module.hot) { module.hot.accept('./router/router', () => { const getRouter = require('./router/router').default; renderWithHotReload(getRouter()); }); } function renderWithHotReload(RootElement) { ReactDOM.render( <AppContainer> {RootElement} </AppContainer>, document.getElementById('app') ) }
-
// webpack.dev.config.js中增加如下 resolve: { // 增加别名设置 alias: { pages : path.join(__dirname, 'src/pages'), router: path.join(__dirname, 'src/router'), component: path.join(__dirname, 'src/component') } }
-
- yarn -D add redux redux-react redux-thunk (异步操作中间件)
/* cd src && mkdir redux 在redux下创建如下目录: |---actions | | | |--userInfo.js | |---reduces | | | |--userInfo.js | |--index.js (合并Reducers的时候用的到) | |---store | | | |--store.js | | 在pages目录下增加一个测试Redux的组件, --> pages/userInfo/userInfo.js */ // 修改index.js代码: function renderWithHotReload(RootElement) { ReactDOM.render( <AppContainer> <Provider store={store}> {RootElement} </Provider> </AppContainer>, document.getElementById('app') ) }
-
- yarn -D add less less-loader style-loader css-loader
// 在webpack.dev.config.js加载器中module中的rules中添加如下配置 { test: /\.css$/, use: ['style-loader', 'css-loader', 'less-loader'], }
-
- yarn -D add url-loader file-loader
// 在webpack.dev.config.js加载器中module中的rules中添加如下配置 { test: /\.(jpg|png|gif)$/, use: [{ loader: 'url-loader', options: { limit: 8192 // <=8K的图片可以直接插入<img/>中 } }] }
-
使用Ant Design of React组件库
-
yarn -D add antd
-
yarn -D add babel-plugin-import
// 注意一个地方,如果要使用Antd.css,需要在css转换器规则下增加一行 { test: /\.css$/, use: ['style-loader', 'css-loader', 'less-loader'], include: path.join(__dirname, 'node_modules/antd') // 导入antd } // 使用按需加载,则不用再显式导入 "plugins": [ "react-hot-loader/babel", ["import", { "libraryName": "antd", "libraryDirectory": "es", "style": "css" // `style: true` 会加载 less 文件 }] ]
-
-
使用Redux DevTools调试工具
- yarn -D add redux-devtools-extension
// '/store/store.js中修改' import {composeWithDevTools} from 'redux-devtools-extension' const store = createStore(rootReducer, composeWithDevTools(applyMiddleware(thunkMiddleware )))
-
-
yarn -D add @babel/plugin-proposal-decorators
-
yarn -D add @babel/plugin-proposal-class-properties
// 在.babelrc中新增 [ "@babel/plugin-proposal-decorators", {"legacy": true} ], [ "@babel/plugin-proposal-class-properties", {"loose": true} ] // 在router下创建bundle.js import React, {Component} from 'react' class Bundle extends Component { state = { // short for "module" but that's a keyword in js, so "mod" mod: null }; componentWillMount() { this.load(this.props) } componentWillReceiveProps(nextProps) { if (nextProps.load !== this.props.load) { this.load(nextProps) } } load(props) { this.setState({ mod: null }); props.load((mod) => { this.setState({ // handle both es imports and cjs mod: mod.default ? mod.default : mod }) }) } render() { return this.props.children(this.state.mod) } } export default Bundle; // 修改路由 import React from 'react'; import Bundle from './bundle' import {BrowserRouter as Router, Route, Switch, Link} from 'react-router-dom'; import Home from 'bundle-loader?lazy&name=home!pages/Home' import Page1 from 'bundle-loader?lazy&name=page1!pages/Page' import UserInfo from 'bundle-loader?lazy&name=userinfo!pages/userInfo/userInfo' import A_date from 'bundle-loader?lazy&name=a_date!pages/Antd/demo01' const Loading = function () { return <div>Loading...</div> }; const createComponent = (component) => (props) => ( <Bundle load={component}> { (Component) => Component ? <Component {...props} /> : <Loading/> } </Bundle> ); const getRouter = () => ( <Router> <div> <ul> <li><Link to="/">首页</Link></li> <li><Link to="/page1">Page1</Link></li> <li><Link to="/userinfo">UserInfo</Link></li> <li><Link to='/Antd'>Antd</Link></li> </ul> <Switch> <Route exact path="/" component={createComponent(Home)}/> <Route path="/page1" component={createComponent(Page1)}/> <Route path="/userinfo" component={createComponent(UserInfo)}/> <Route path="/Antd" component={createComponent(A_date)} /> </Switch> </div> </Router> ); export default getRouter;
-
-
/* 假如用户第一次访问页面后,采用缓存机制,缓存了旧的home.js , 那如果当更新了home.js后.则会导致用户出现加载 错误,解决如下。修改webpack.dev.config.js */ output: { path: path.join(__dirname, './dist'), filename: '[name].[hash].js', // 处理缓存 chunkFilename: '[name].[chunkhash].js' // 区分加载的js }, /* 配合HtmlWebpackPlugin模块 yarn -D add html-webpack-plugin // 修改webpack.dev.config.js中的plugins配置 */ plugins: [ new Webpack.HotModuleReplacementPlugin(), //启动热替换 new HtmlWebpackPlugin({ filename: 'index.html', template: path.join(__dirname, './src/index.html') }) ],
-
// 修改如下两处地方 entry: { app: [ 'react-hot-loader/patch', // 热更替 path.join(__dirname, 'src/index.js') ], //++ verdor: ['react','react-redux','react-dom','react-router-dom','redux'] // 提取公共代码 }, // ++ new Webpack.LoaderOptionsPlugin({ // ++ optimzation: { // ++ splitChunks: { // ++ name: 'vendor' // ++ } // ++ }, // ++ })
-
- vim webpack.config.js
/* 增加如下内容(大体上与开发模式相同) : 1: 去除热更提 2: 模式修改为生产模式 */ const path = require('path') const Webpack = require('webpack') const HtmlWebpackPlugin = require('html-webpack-plugin') module.exports = { mode: "production", // 开发模式 entry: { app: [ path.join(__dirname, 'src/index.js') ], verdor: ['react','react-redux','react-dom','react-router-dom','redux'] // 提取公共代码 }, output: { path: path.join(__dirname, './dist'), filename: '[name].[chunkhash].js', // 处理缓存 chunkFilename: '[name].[chunkhash].js' // 区分加载的js }, module: { rules: [ { test: /\.js$/, use: ['babel-loader?cacheDirectory=true'], // cacheDirectory=true开启缓存机制,加快编译速度 include: path.join(__dirname, './src') }, { test: /\.css$/, use: ['style-loader', 'css-loader', 'less-loader'], include: path.join(__dirname, '/node_modules/antd') // 处理Antd.css }, { test: /\.(jpg|png|gif)$/, use: { loader: 'url-loader', options: { limit: 8192 // <=8K的图片可以直接插入<img/>中 } } } ] }, plugins: [ new HtmlWebpackPlugin({ filename: 'index.html', template: path.join(__dirname, './src/index.html') }), new Webpack.LoaderOptionsPlugin({ optimzation: { splitChunks: { name: 'vendor' } }, }) ], resolve: { // 增加别名设置 alias: { pages: path.join(__dirname, 'src/pages'), router: path.join(__dirname, 'src/router'), component: path.join(__dirname, 'src/component'), actions: path.join(__dirname, 'src/redux/actions'), reducers: path.join(__dirname, 'src/redux/reducers'), } }, devtool: 'inline-source-map' }
-
- yarn -D add uglifyjs-webpack-plugin
//修改webpack.config.js // ++ const UglifyJSPlugin = require('uglifyjs-webpack-plugin') // ++ new UglifyJSPlugin()
-
plugins: [ // ++ new Webpack.DefinePlugin({ // 指定环境,用于针对特定的环境进行一些优化 // ++ 'process.env': { // ++ 'NODE_ENV': JSON.stringify('production') // ++ } // ++ }) ]
-
- yarn -D add clean-webpack-plugin
plugins: [ // ++ new webpack.HashedModuleIdsPlugin() // 使vendor.xxx.js缓存在本地 // ++ new clean-webpack-plugin() // 用于在构建之前清除构建文件夹 ]
-
- yarn -D add extract-text-webpack-plugin
rules: [ { test: /\.css$/, // -- use: ['style-loader', 'css-loader', 'less-loader'], // ++ use: ExtractTextWebpackPlugin.extract({ // ++ fallback: 'style-loader', // ++ use: ['css-loader','less-loader'] // ++ }) include: path.join(__dirname, '/node_modules/antd') // 处理Antd.css }, ] plugins : [ // ++ new ExtractTextWebpackPlugin({ // ++ filename: 'style.css' // ++ allchunks: true // ++ }) ]
-
- yarn -D add axios
- cd src/middleware && mkdir middleware && cd middleware && vim promiseMiddleware.js
/* promiseMiddleware.js(源码) */ // 请求中间件 import axios from 'axios' export default store => next => action => { const {dispatch, getState} = store; // 如果是函数直接运行跳过 if(typeof action === 'function'){ action(dispatch, getState) return; } // 解析action console.log('action->',action); const { promise, types, afterSuccess, ...rest } = action; if(!action.promise){ return next(action); } // 解析types const [ REQUEST, SUCCESS, FALID ] = types; // 请求流程 next({ ...rest, type: REQUEST }); const onSuccess = result => { next({ ...rest, result, type: SUCCESS }) console.log(result); if(afterSuccess){ afterSuccess(dispatch, getState, result) } } const onReject = error => { next({ ...rest, error, type: FALID }) } return promise(axios).then(onSuccess,onReject).catch(error => { console.log(`捕获错误: ${error}`); onReject(error) }) } /* store.js */ // ++ import promiseMiddleware from '../middleware/promiseMiddleware' // ++ const store = createStore(rootReducer, composeWithDevTools(applyMiddleware(promiseMiddleware))) /* actions/userInfo.js */ // const getUserInfoRequest = () => { // -- return { // -- type: GET_USER_INFO_REQUEST, // -- } // -- } // -- const getUserInfoSuccess = (userInfo) => { // -- return { // -- type: GET_USER_INFO_SUCCESS, // -- payload: userInfo, // -- } // -- } // -- const getUserInfoFaild = () => { // -- return { // -- type: GET_USER_INFO_FAILD, // -- } // -- } // -- export const getUserInfo = () => { // -- return (dispatch) => { // -- // 开始请求前 // -- dispatch(getUserInfoRequest()) // -- return fetch('http://localhost:8080/api/user.json') // -- .then(response => response.json()) // -- .then(json => { // -- // 获取数据 // -- dispatch(getUserInfoSuccess(json)) // -- }).catch(() => { // -- // 捕获错误 // -- dispatch(getUserInfoFaild()) // -- }) // -- } // -- } // ++ export function getUserInfo(){ // ++ return{ // ++ types: [GET_USER_INFO_REQUEST,GET_USER_INFO_SUCCESS,GET_USER_INFO_FAILD], // ++ promise: axios => axios.get('http://localhost:8080/api/user.json') // ++ } // ++ } /* reducers/userInfo.js */ case GET_USER_INFO_SUCCESS: return { ...state, isLoading: false, // ++ userInfo: action.payload, errMessage: '' }
-
- yarn -D add webpack-merge
- vim webpack.common.js
/* 假如在webpack.dev.config.js中修改了css加载器,那么在webpack.config.js中也需要同理修改. 所以把这两个webpack中相同的配置提取出来. */
-
- 💫待定(省略)
-
/*index.js*/ // ++ redux模块热替换 // ++ module.hot.accept("./redux/reducers/userInfo.js", () => { // ++ const nextCombineReducers = require("./redux/reducers/userInfo.js").default; // ++ store.replaceReducer(nextCombineReducers); // ++ });
-
- yarn -D add mockjs
- http://jsonplaceholder.typicode.com/(这个网站也能模拟请求JSON数据)
-
- 💫待定(省略)
-
-
🌱 基本骨架就是这样。文件目录没有做统一,因为大家的习惯可能不一样。后续会补充如下部分:
-
Antd
-
React Native
-
配合Express
-
-
-
优化Babelrc配置
-
补充Ant Design of React
-
补充react-router
-
补充使用axios和middleware优化API请求(中间件)
-
补充优化目录结构
-
模拟Ajax数据
/** 补充Ant Design of React * 1:表格 * 2: 表单组件 * 3: 上传 */ // 表格 : // 一般后台上传后会进入上传管理表格,那么可以抽出一个表单管理基础组件跟表单管理高级组件的公共组件机制 // 基础组件没有删除,没有编辑,很纯粹的基础表格组件例如打印日志 // 高级组件拥有基础组件里没有的功能 // 如下代码分别高级公共表格组件 , 基础公共表格组件 /** * @description: 高级表格公共组件 * @param: 需要从父组件获取表个头配置,接口地址,一个用于修改,删除的对象 */ import React,{useState, useEffect} from 'react'; import {Table, Input, Button, Popconfirm, Form, message} from 'antd'; import { async } from 'rxjs/internal/scheduler/async'; const FormItem = Form.Item; // 创建Context实例 const EditableContext = React.createContext(); // 创建生产者 const EditableRow = ({form, index, ...props}) => ( <EditableContext.Provider value={form}> <tr {...props} /> </EditableContext.Provider> ) const EditableFormRow = Form.create()(EditableRow); // EditableCell const EditableCell = (props) => { const getInput = () => { return <Input />; }; const { editing, dataIndex, title, inputType, record, index, ...restProps } = props; return ( <EditableContext.Consumer> {(form) => { const { getFieldDecorator } = form; return ( <td {...restProps}> {editing ? ( <FormItem style={{ margin: 0 }}> {getFieldDecorator(dataIndex, { rules: [{ required: true, message: `Please Input ${title}!`, }], initialValue: record[dataIndex], })(getInput())} </FormItem> ) : restProps.children} </td> ); }} </EditableContext.Consumer> ); } // EditableTable const EditableTable = (props) => { // 接口地址 const [ GET_ALL_DATA, DELETE_ALL_DATA, UPDATE_ALL_DATA ] = props.interfaceUrl // 设置初始值 const [page, setPage] = useState({}), [data, setData] = useState(null), [filterdata, setFilterdata] = useState(null), [editingKey, setEditingKey] = useState(''); // 分页有关 const [pageNum, setPageNum] = useState(null), [pageSize, setPageSize] = useState(null), [sortedInfo, setSortedInfo] = useState(null); // 填充表格数据 useEffect(() => { getData() },[]) const getData = async () => { await GET_ALL_DATA().then((res) => { if(res && res.status === 200 ){ setPage({ total: res.data.length, pageNum: 1 }); setData(res.data); }else{ message.error(res.message); setPage({ total: 1, pageNum: 1 }); setData([]); } }) } // 改变页码 const changePage = (current) => { setPageNum(current) } // 每页显示多少条 const changePageSize = (pageSize, current) => { setPageSize(pageSize) } // 删除操作 const HandleDelete = (key) => { DELETE_ALL_DATA(key).then( (res , err) => { if(res.status === 200){ message.success(`编号${key}已经删除成功`); setData([...data].filter(item => item.key !== key)); setEditingKey('') // props.clearDeleteKey() }else{ message.error('删除捕获错误:', err); } }) } // 从父组件获取要删除的key,触发handleDelete const deleteKeyFromFather = props.deleteKeyArr; if( deleteKeyFromFather instanceof Array && deleteKeyFromFather.length > 0){ React.memo(HandleDelete(deleteKeyFromFather)) } // 排序操作 const handleChange = (sorter) => { setSortedInfo(sorter) } // 操作触发器 const isEditing = record => record.key === editingKey; // Edit设置 const edit = (key) => setEditingKey(key); // 保存修改 const save = (form, key) => { form.validateFields((error, row) => { if (error) { return; } const newData = [...data]; const index = newData.findIndex(item => key === item.key); if (index > -1) { const item = newData[index]; newData.splice(index, 1, { ...item, ...row, }); const requestData = { ...item, ...row }; /** * @description 传输给后端接口 * @param {wheelNo, paramType} 处理几个上传到后端的特殊字段 **/ const _portParam = props._portParam; const _requestData = {}; for(let i in _portParam){ _requestData[i] = requestData[i]; if(i === 'wheelNo'){ _requestData[i] = Number(requestData[i]) } if(i === 'paramType'){ _requestData[i] = 4 } } UPDATE_ALL_DATA(_requestData).then((res, err) => { console.log('status=>>',res) if(res.status === 200){ message.success('修改成功') }else{ message.error(res.message) } }) setData(newData); setEditingKey('') } else { newData.push(row); setData(newData); setEditingKey('') } }); } // 行选择 const rowSelection = { onChange: (selectedRowKeys, selectedRows) => { const selectRowKeysArr = Array.from(selectedRowKeys); // 激活删除按钮 selectedRows.length !== 0 ? props.callFn(false, selectRowKeysArr) : props.callFn(true) }, getCheckboxProps: record => ({ disabled: record.name === 'Disabled User', name: record.name }) } // 取消修改 const cancel = () => setEditingKey(''); const totals = page.total; // 表格分页属性 const paginationProps = { showSizeChanger: true, showQuickJumper: true, showTotal: () => `共${totals}条`, pageSize: pageSize, current: pageNum, total: page.total, onShowSizeChange: (current,pageSize) => changePageSize(pageSize,current), onChange: (current) => changePage(current), }; // 覆盖默认的table元素 const components = { body: { row: EditableFormRow, cell: EditableCell, }, }; // columns表格头(动态导入) const columns = props.columns.map((col) => { if (!col.editable) { return col; } return { ...col, onCell: record => ({ record, dataIndex: col.dataIndex, title: col.title, editing: isEditing(record) }), }; }) // 单独处理操作的render(因为硬处理太困难) const dealOperateFn = () => { const columnList = props.columns; columnList.forEach((item) => { if(item.title === '操作'){ item.render = (text, record) => { const editable = isEditing(record); return ( <div> {editable ? ( <span> <EditableContext.Consumer> {form => ( <Button onClick={() => save(form, record.key)} > 保存修改 </Button> )} </EditableContext.Consumer> <Popconfirm title="确认取消修改吗?" onConfirm={() => cancel(record.key)} > <Button style = {{ margin: '5px 0px' }}> 取消修改 </Button> </Popconfirm> <Popconfirm title="确定要删除吗?" onConfirm={() => HandleDelete(record.key)}> <a href="javascript:;"> <Button type="danger" className="deleteButton">删除</Button> </a> </Popconfirm> </span> ) : ( <a disabled={editingKey !== ''} onClick={() => edit(record.key)}> <Button type="primary"> 设置 </Button> </a> )} </div> ) } } }) } dealOperateFn(); // 渲染 return( <EditableContext.Provider value={props.form}> <Table components={components} rowClassName={() => 'editable-row'} bordered dataSource={data} columns={columns} pagination = {paginationProps} onChange = {handleChange} rowSelection = {rowSelection} /> </EditableContext.Provider> ) } export default EditableTable // 基础公共组件
-