面對日新月異的前端,我表示快學不動了?。 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'//輸出檔案新增hash
},
optimization: { // 代替commonchunk, 程式碼分割
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,//css modules
localIdentName: '[name]___[local]___[hash:base64:5]'
},
},
'postcss-loader', 'sass-loader']
},
{ /*
當檔案體積小於 limit 時,url-loader 把檔案轉為 Data URI 的格式內聯到引用的地方
當檔案大於 limit 時,url-loader 會呼叫 file-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()//HMR
]
};
接著基於 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
// 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' },
]
};
// useReducer對映出state,dispatch
const [state, dispatch] = useReducer(rootReducer, initState);
// <Context.Provider value={{ state, dispatch }}> 基本代替了 react-redux 的 <Provider store={store}>
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 與之前的寫法是一致的。
// constants.js
export const ADD_COMMENT = 'ADD_COMMENT'
export const REMOVE_COMMENT = 'REMOVE_COMMENT'
action.js
// 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
//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
//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]); //不傳第二個引數的話,它就等價於每次componentDidMount和componentDidUpdate時執行 useEffect(() => { updateData(); }); //第二個引數傳空陣列,等價於只在componentDidMount和componentWillUnMount時執行, //第一個引數中的返回函式用於執行清理功能 useEffect(() => { initData(); reutrn () => console.log('componentWillUnMount cleanup...'); }, []);
最後就是實現具體介面和業務邏輯的元件了,下面是其中的List元件
// list.js
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);//獲取全域性狀態state
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;