基於 Webpack 4 和 React hooks 搭建專案

Jeff.Zhong發表於2019-02-04

面對日新月異的前端,我表示快學不動了?。 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

狀態管理元件使用 reduxreact-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.jsaction.jsreducer.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;

專案程式碼

https://github.com/edwardzhong/webpack_react

相關文章