基于 Webpack 4 和 React hooks 搭建项目

面对日新月异的前端,我表示快学不动了😂。 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;

项目代码

https://github.com/edwardzhong/webpack_react

posted @ 2019-02-04 13:54  Jeff.Zhong  阅读(2317)  评论(0)    收藏  举报