基於 Webpack 4 和 React hooks 搭建專案
面對日新月異的前端,我表示快學不動了:joy:。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 。
<Context.Provider value={{ state, dispatch }}>基本代替了react-redux
的 **
// 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); 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 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' 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;