面对日新月异的前端,我表示快学不动了😂。 Webpack 老早就已经更新到了 V4.x,前段时间 React 又推出了 hooks API。刚好春节在家里休假,时间比较空闲,还是赶紧把 React技术栈这块补上。
网上有很多介绍 hooks 知识点的文章,但都比较零碎,基本只能写一些小 Demo。还没有比较系统的,全新的基于 hooks 进行搭建实际项目的讲解。所以这里就从开发实际项目的角度,搭建起单页面 Web App项目的基本脚手架,并基于 hooks API 实现一个 react 项目模版。
Hooks 最吸引人的地方就是用 函数式组件 代替面向对象的 类组件。此前的 react 如果涉及到状态,解决方案通常只能使用 类组件,业务逻辑一复杂就容易导致组件臃肿,模块的解藕也是个问题。而使用基于 hooks 的 函数组件 后,代码不仅更加简洁,写起来更爽,而且模块复用也方便得多,非常看好它的未来。
webpack 4 的配置
没有使用 create-react-app 这个脚手架,而是从头开始配置开发环境,因为这样自定义配置某些功能会更方便些。下面这个是通用的配置 webpack.common.js 文件。
const { resolve } = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const CleanWebpackPlugin = require('clean-webpack-plugin'); const { HotModuleReplacementPlugin } = require('webpack');
module.exports = { entry: './src/index.js', output: { path: resolve(__dirname, 'dist'), filename: '[name].[hash].js' }, optimization: { runtimeChunk: 'single', splitChunks: { cacheGroups: { vendor: { test: /[\\/]node_modules[\\/]/, name: 'vendors', chunks: 'all' } } } }, module: { rules: [ { test: /\.jsx?$/, exclude: /node_modules/, use: ['babel-loader'] }, { test: /\.css$/, use: ['style-loader', 'css-loader'] }, { test: /\.scss$/, use: ['style-loader', { loader: 'css-loader', options: { importLoaders: 1, modules: true, localIdentName: '[name]___[local]___[hash:base64:5]' }, }, 'postcss-loader', 'sass-loader'] }, {
test: /\.(png|jpg|jpeg|gif|eot|ttf|woff|woff2|svg|svgz)(\?.+)?$/, use: [{ loader: 'url-loader', options: { limit: 1000 } }] } ] }, plugins: [ new CleanWebpackPlugin(['dist']), new HtmlWebpackPlugin({ template: './public/index.html', favicon: './public/favicon.png', minify: { removeAttributeQuotes:true, removeComments: true, collapseWhitespace: true, removeScriptTypeAttributes:true, removeStyleLinkTypeAttributes:true }, }), new HotModuleReplacementPlugin() ] };
|
接着基于 webpack.common.js 文件,配置出开发环境的 webpack.dev.js 文件,主要就是启动开发服务器。
const merge = require('webpack-merge'); const common = require('./webpack.common.js');
module.exports = merge(common, { mode: 'development', devtool: 'inline-source-map', devServer: { contentBase: './dist', port: 4001, hot: true } });
|
生成模式的 webpack.prod.js 文件,只要定义了 mode:’production’, webpack 4 打包时就会自动压缩优化代码。
const merge = require('webpack-merge'); const common = require('./webpack.common.js');
module.exports = merge(common, { mode: 'production', devtool: 'source-map' });
|
配置 package.js 中的 scripts
{ "scripts": { "start": "webpack-dev-server --open --config webpack.dev.js", "build": "webpack --config webpack.prod.js" } }
|
Babel 的配置
babel 的 .babelrc 文件, css module 包这里推荐 *babel-plugin-react-css-modules。
*react-css-modules** 既支持全局的css(默认 className 属性),同时也支持局部css module( styleName 属性),还支持css预编译器,这里使用的是 scss。
{ "presets": [ "@babel/preset-env", "@babel/preset-react" ], "plugins": [ "@babel/plugin-proposal-class-properties", "@babel/plugin-transform-runtime", [ "react-css-modules", { "exclude": "node_modules", "filetypes": { ".scss": { "syntax": "postcss-scss" } }, "generateScopedName": "[name]___[local]___[hash:base64:5]" } ] ] }
|
React 项目
下面是项目基本的目录树结构,接着从入口开始一步步细化整个项目。
├ package.json ├ src │ ├ component // 组件目录 │ ├ reducer // reducer目录 │ ├ action.js │ ├ constants.js │ ├ context.js │ └ index.js ├ public // 静态文件目录 │ ├ css │ └ index.html ├ .babelrc ├ webpack.common.js ├ webpack.dev.js └ webpack.prod.js
|
状态管理组件使用 redux, react-router 用于构建单页面的项目,因为使用了 hooks API,所以不再需要 react-redux 连接状态 state。
入口文件 index.js
import React, { useReducer } from 'react' import { render } from 'react-dom' import { HashRouter as Router, Route, Redirect, Switch } from 'react-router-dom' import Context from './context.js' import Home from './component/home.js' import List from './component/list.js' import rootReducer from './reducer' import '../public/css/index.css'
const Root = () => { const initState = { list: [ { id: 0, txt: 'webpack 4' }, { id: 1, txt: 'react' }, { id: 2, txt: 'redux' }, ] }; const [state, dispatch] = useReducer(rootReducer, initState); return <Context.Provider value={{ state, dispatch }}> <Router> <Switch> <Route exact path="/" component={Home} /> <Route exact path="/list" component={List} /> <Route render={() => (<Redirect to="/" />)} /> </Switch> </Router> </Context.Provider> } render( <Root />, document.getElementById('root') )
|
constants.js , action.js 和 reducer.js 与之前的写法是一致的。
export const ADD_COMMENT = 'ADD_COMMENT' export const REMOVE_COMMENT = 'REMOVE_COMMENT'
|
action.js
import { ADD_COMMENT, REMOVE_COMMENT } from './constants'
export function addComment(comment) { return { type: ADD_COMMENT, comment } }
export function removeComment(id) { return { type: REMOVE_COMMENT, id } }
|
list.js
import { ADD_COMMENT, REMOVE_COMMENT } from '../constants.js'
const list = (state = [], payload) => { switch (payload.type) { case ADD_COMMENT: if (Array.isArray(payload.comment)) { return [...state, ...payload.comment]; } else { return [...state, payload.comment]; } case REMOVE_COMMENT: return state.filter(i => i.id != payload.id); default: return state; } }; export default list
|
reducer.js
import { combineReducers } from 'redux' import list from './list.js' import user from './user.js'
const rootReducer = combineReducers({ list, user });
export default rootReducer
|
最大区别的地方就是 component 组件,基于 函数式,内部的表达式就像是即插即用的插槽,可以很方便的抽取出通用的组件,然后从外部引用。相比之前的 面向对象 方式,我觉得 函数表达式 更受前端开发者欢迎。
useContext 获取全局的 state
useRef 代替之前的 ref
useState 代替之前的 state
useEffect 则可以代替生命周期钩子函数
useEffect(() => { updateData(); },[id]);
useEffect(() => { updateData(); });
useEffect(() => { initData(); reutrn () => console.log('componentWillUnMount cleanup...'); }, []);
|
最后就是实现具体界面和业务逻辑的组件了,下面是其中的List组件
import React, { useRef, useState, useContext } from 'react' import { bindActionCreators } from 'redux' import { Link } from 'react-router-dom' import Context from '../context.js' import * as actions from '../action.js' import Dialog from './dialog.js' import './list.scss'
const List = () => { const ctx = useContext(Context); const { user, list } = ctx.state; const [visible, setVisible] = useState(false); const [rid, setRid] = useState(''); const inputRef = useRef(null); const { removeComment, addComment } = bindActionCreators(actions, ctx.dispatch);
const confirmHandle = () => { setVisible(false); removeComment(rid); }
const cancelHandle = () => { setVisible(false); }
const add = () => { const input = inputRef.current, val = input.value.trim(); if (!val) return; addComment({ id: Math.round(Math.random() * 1000000), txt: val }); input.value = ''; }
return <> <div styleName="form"> <h3 styleName="sub-title">This is list page</h3> <div> <p>hello, {user.name} !</p> <p>your email is {user.email} !</p> <p styleName="tip">please add and remove the list item !!</p> </div> <ul> { list.map(l => <li key={l.id}>{l.txt}<i className="icon-minus" title="remove item" onClick={() => { setVisible(true); setRid(l.id); }}></i></li>) } </ul> <input ref={inputRef} type="text" /> <button onClick={add} title="add item">Add Item</button> <Link styleName="link" to="/">redirect to home</Link> </div> <Dialog visible={visible} confirm={confirmHandle} cancel={cancelHandle}>remove this item ?</Dialog> </> }
export default List;
|
项目代码
https://github.com/edwardzhong/webpack_react