React 折騰記 - (5) 記錄用React開發專案過程遇到的問題(Webpack4/React16/antd等)

CRPER發表於2018-10-29

前言

自己搭的腳手架,坑都是一步一步踩完的;

技術棧: react@16.6.0/ react-router-dom@v4 / webpack^4.23.1(babel7+)

閒話不多說,直入主題,有興趣的可以瞧瞧,沒興趣的止步,節約您的時間.


問題列表

問題一:history模式下,介面和請求衝突的問題

就是反向對映介面和請求的根路徑重疊,如下:


        proxy: {
            '/': {
                target: 'http://192.168.31.100/api/web',
                changeOrigin: true,
                secure: false,
            }
        },

複製程式碼

這樣對映會造成路由定址不到...

這個問題我遇到的時候,浪費了挺多時間,最後發現還是有解的;

網上大多數人的寫法就是,加個prefix(聚合一個字首),然後用pathRewrite重寫請求路徑

        proxy: {
            '/api': {
                target: 'http://192.168.31.100/api/web',
                changeOrigin: true,
                secure: false,
                pathRewrite: { '^/api': '/' },
            }
        },
        historyApiFallback: true 

複製程式碼

可這法子,不大適合我這邊...能不能重疊又不影響,

翻了一些Stack Overflow上的問答和文件,發現還是有的.

下面的寫法就是先判斷是html請求還是其他請求,若是請求html則不反向代理

        proxy: {
            '/': {
                target: 'http://192.168.31.100/api/web',
                changeOrigin: true,
                secure: false,
                // pathRewrite: { '^/api': '/' },
                bypass: function(req, res, proxyOptions) {
                    if (req.headers.accept.indexOf('html') !== -1) {
                        console.log('Skipping proxy for browser request.');
                        return '/index.html';
                    }
                }
            }
        },
        historyApiFallback: true 

複製程式碼

問題二: 如何非ts下支援裝飾器 , 以及常規的語法解析

因為用了mobx,實在不想用高階函式的寫法..一堆括號..

我是直接配置babelrc的. 跟隨最新babel 7,裝上這個依賴即可支援

  • @babel/plugin-proposal-decorators -- 裝飾器支援
  • @babel/plugin-syntax-dynamic-import -- 動態引入相關程式碼,適用於程式碼分離
  • babel/plugin-proposal-object-rest-spread -- ...的支援
  • @babel/plugin-proposal-class-properties -- class支援
  • babel-plugin-import -- 阿里出品的css 按需載入
  • react-hot-loader/babel -- 配置react-hot-loader會用到
{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": {
          "browsers": [
            "last 3 versions",
            "safari >= 7"
          ]
        },
        "modules": false,
        "debug": false,
        "useBuiltIns": "usage"
      }
    ],
    "@babel/preset-react"
  ],
  "plugins": [
    [
      "@babel/plugin-proposal-decorators",
      {
        "legacy": true
      }
    ],
    [
      "@babel/plugin-proposal-class-properties",
      {
        "loose": true
      }
    ],
    "@babel/plugin-proposal-object-rest-spread",
    [
      "import",
      {
        "libraryName": "antd",
        "libraryDirectory": "es",
        "style": "css"
      }
    ],
    "@babel/plugin-syntax-dynamic-import",
    "react-hot-loader/babel"
  ]
}

複製程式碼

問題三: mobx實現路由基礎鑑權

  • model
import { observable, action, computed, toJS } from 'mobx';
import API from 'services';  // axios的封裝

class AuthModel {
    constructor() {}

    // 登入請求
    @action
    requestLogin = async values => {
        // 登入介面
        const data = await API.post('/admin/login', values);
        const AuthUserData = JSON.stringify(data);
        window.localStorage.setItem('AuthUserData', AuthUserData);
        window.location.href = '/';
    };

    // 退出登入
    @action
    requestLogout = async values => {
        this.UserData = {}; // 重置為空物件
        this.isPermission = false;
        window.localStorage.removeItem('AuthUserData');
        window.location.href = '/entrance/login';
    };

    @computed
    get isAuthenticated() {
        if (window.localStorage.getItem('AuthUserData')) {
            if (JSON.parse(window.localStorage.getItem('AuthUserData')).token) {
                return true;
            }
            return false;
        } else {
            return false;
        }
    }
}

const Auth = new AuthModel();

export default Auth;


複製程式碼
  • 在對應的入口引入,結合react-route-domswitch跳轉

import React, { Component } from 'react';
import { hot } from 'react-hot-loader';
import DevTools from 'mobx-react-devtools';
import { BrowserRouter, Route, Switch, Redirect } from 'react-router-dom';
import { observer, inject } from 'mobx-react';
import './App.css';
import ErrorBoundary from 'components/ErrorBoundary/ErrorBoundary';
import asyncComponent from 'components/asyncComponent/asyncComponent';

// 登入註冊找回密碼
const Entrance = asyncComponent(() => import('pages/Entrance/Entrance'));

// 管理後臺
import AdminLayout from 'pages/Layout/AdminLayout';

@inject('auth')
@observer
export class App extends Component {
    constructor(props) {
        super(props);
    }
    componentDidMount = () => {};

    render() {
        const { isAuthenticated } = this.props.auth;
        return (
            <ErrorBoundary>
                <BrowserRouter>
                    <div>
                        <Switch>
                            <Route
                                path="/entrance"
                                render={() =>
                                    isAuthenticated ? (
                                        <Redirect exact to="/" />
                                    ) : (
                                        <Entrance />
                                    )
                                }
                            />
                            <Route
                                path="/"
                                render={() =>
                                    isAuthenticated ? (
                                        <AdminLayout />
                                    ) : (
                                        <Redirect exact to="/entrance" />
                                    )
                                }
                            />
                        </Switch>
                        {/**這裡是開啟了開發模式下顯示mobx的devtool*/}
                        {process.env.NODE_ENV === 'development' ? (
                            <DevTools />
                        ) : null}
                    </div>
                </BrowserRouter>
            </ErrorBoundary>
        );
    }
}

// react-hot-loader v4的寫法
export default hot(module)(App);


複製程式碼

問題四: 加快開發模式下的編譯,以及常規的美化輸出

用了happypack來加快了js,css的編譯速度(多程式);

css也開啟了tree shaking

我這個專案沒有引入lesssass,用styled-components@4來寫樣式

  • webpack.base.config.js

const webpack = require('webpack');
const path = require('path');

// 終端輸出進度條
const WebpackBar = require('webpackbar');

// html模板
const HtmlWebpackPlugin = require('html-webpack-plugin');

// css 抽離
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

// 多程式編譯
const HappyPack = require('happypack');
const os = require('os');
const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length });

// 給指令碼預新增資訊
const ScriptExtHtmlWebpackPlugin = require('script-ext-html-webpack-plugin');

// css tree shaking

const glob = require('glob');
const PurifyCSSPlugin = require('purifycss-webpack');

// 顯示編譯時間
const ProgressBarPlugin = require('progress-bar-webpack-plugin');
const chalk = require('chalk');

const config = {
    entry: [path.resolve(__dirname, '../src')],
    resolve: {
        extensions: ['.js', '.jsx'],
        modules: [path.resolve(__dirname, '../src'), 'node_modules'],
        alias: {
            store: path.resolve(__dirname, '..', 'src/store'),
            transition: path.resolve(__dirname, '..', 'src/transition'),
            components: path.resolve(__dirname, '..', 'src/components'),
            utils: path.resolve(__dirname, '..', 'src/utils'),
            pages: path.resolve(__dirname, '..', 'src/pages'),
            views: path.resolve(__dirname, '..', 'src/views'),
            services: path.resolve(__dirname, '..', 'src/services'),
            assets: path.resolve(__dirname, '..', 'src/assets'),
            router: path.resolve(__dirname, '..', 'src/router')
        }
    },
    performance: {
        hints: false
    },
    plugins: [
        // 顯示打包時間
        new ProgressBarPlugin({
            format:
                '  build [:bar] ' +
                chalk.green.bold(':percent') +
                ' (:elapsed seconds)'
        }),
        // css tree shaking
        new PurifyCSSPlugin({
            // 路勁掃描 nodejs內建 路勁檢查
            paths: glob.sync(path.join(__dirname, 'pages/*/*.html'))
        }),
        // 進度條
        new WebpackBar(),
        // 定製全域性變數
        new webpack.DefinePlugin({
            'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV)
        }),
        // 生成引用一個或多個出口檔案的html,需要生成多少個 html 就 new 多少此該外掛
        new HtmlWebpackPlugin({
            // 沒有引入模板時的預設title,favicon也一樣,但filename除外
            title: 'index',
            favicon: path.resolve(__dirname, '../public/favicon.png'),
            // 定義插入到文件哪個節點,預設在body倒數位置
            inject: 'body',
            filename: 'index.html',
            template: path.resolve(__dirname, '../public/index.html'),
            // 壓縮html檔案
            // 詳細的配置 https://github.com/kangax/html-minifier#options-quick-reference
            minify:
                process.env.NODE_ENV === 'production'
                    ? {
                          removeComments: true,
                          collapseWhitespace: true,
                          removeRedundantAttributes: true,
                          useShortDoctype: true,
                          removeEmptyAttributes: true,
                          removeStyleLinkTypeAttributes: true,
                          keepClosingSlash: true,
                          minifyJS: true,
                          minifyCSS: true,
                          minifyURLs: true
                      }
                    : {},
            // 在js檔案後面加上一段hash碼,預設為false
            hash: true
        }),
        new HappyPack({
            id: 'js',
            threadPool: happyThreadPool,
            loaders: ['babel-loader?cacheDirectory=true']
        }),
        new HappyPack({
            id: 'css',
            threadPool: happyThreadPool,
            loaders: [
                {
                    loader: 'css-loader',
                    options: {
                        importLoaders: 1 // 0 => 無 loader(預設); 1 => postcss-loader; 2 => postcss-loader, sass-loader
                    }
                },
                'postcss-loader'
            ]
        }),
        new MiniCssExtractPlugin({
            // Options similar to the same options in webpackOptions.output
            // both options are optional
            filename:
                process.env.NODE_ENV !== 'production'
                    ? 'static/css/[name].css'
                    : 'static/css/[name].[hash].css',
            chunkFilename:
                process.env.NODE_ENV !== 'production'
                    ? 'static/css/[id].css'
                    : 'static/css/[id].[hash].css'
        })
    ],
    module: {
        rules: [
            {
                test: /\.js$/,
                include: [path.resolve(__dirname, '../src')],
                exclude: /node_modules/,
                use: 'happypack/loader?id=js'
            },
            {
                test: /\.css$/,
                loaders: [
                    'style-loader',
                    // {
                    //     loader: 'css-loader',
                    //     options: {
                    //         importLoaders: 1 // 0 => 無 loader(預設); 1 => postcss-loader; 2 => postcss-loader, sass-loader
                    //     }
                    // },
                    // 'postcss-loader'
                    'happypack/loader?id=css'
                ]
            },
            {
                test: /\.json$/,
                loader: 'file-loader',
                options: {
                    name: 'json/[name].[ext]',
                    outputPath: 'static'
                }
            },
            {
                test: /\.(jpe?g|png|gif)(\?.*)?$/,
                include: [path.resolve(__dirname, '../src/assets/')],
                exclude: /node_modules/,
                use: [
                    {
                        loader: 'file-loader',
                        options: {
                            name: '/images/[name].[ext]?[hash]',
                            outputPath: 'static'
                        }
                    }
                ]
            },
            {
                test: /\.(eot|svg|ttf|woff|woff2)$/,
                use: [
                    {
                        loader: 'file-loader',
                        options: {
                            name: 'fonts/[name].[ext]',
                            outputPath: 'static'
                        }
                    }
                ]
            }
        ]
    }
};

module.exports = config;
複製程式碼
  • 效果圖如下

React 折騰記 - (5) 記錄用React開發專案過程遇到的問題(Webpack4/React16/antd等)


問題五: 用新的getDerivedStateFromProps取代componentWillReceiveProps?

新的寫法是組合寫法,若是隻用這個靜態方法有時候會造成無限迴圈渲染,導致堆疊溢位

一旦用了static getDerivedStateFromProps(nextProps, prevState) ,必須返回一個值,

若是不更新state,那就返回null;

有時候在這裡返回新的state不夠嚴謹,這時候就要結合componentDidUpdate來變得更可控

componentDidUpdate = (prevProps, prevState, snapshot) 這個生命週期的第三個引數

是用來捕獲更新前的state(其實就是getDerivedStateFromProps返回的)


問題六: antd上傳元件結合axios上傳失敗

這個問題,挺坑的...antd官方文件說了可以設定header,

headerform-data就掛了(預設就是這個提交格式)

最終axios裡面還要過濾下,在請求攔截器裡面



// 產生一個基於 axios 的新例項
const api = axios.create({
    baseURL: process.env.NODE_ENV === 'development' ? isDev : isProd, // 介面根路徑
    timeout: 5000, // 超時時間
    withCredentials: true, // 是否跨站點訪問控制請求,攜帶 cookie
    responseType: 'json', // 響應資料格式
    headers: {
        // 設定請求頭cd
        'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8'
    }
});

// http請求攔截器
api.interceptors.request.use(
    config => {
        // 請求開始,藍色過渡滾動條開始出現
        NProgress.start();
        if (window.localStorage.getItem('AuthUserData')) {
            let token =
                '不給你看 ' +
                JSON.parse(window.localStorage.getItem('AuthUserData')).token;
            config.headers.Authorization = token;
        }

        if (
            config.method === 'post' ||
            config.method === 'put' ||
            config.method === 'patch'
        ) {
            //  這段就是問答的解決所在,識別為該格式的資料,不用`qs`編碼,直接提交
            if (config.headers['Content-Type'] === 'multipart/form-data') {
                return config;
            }

            // 若是需要對介面的欄位進行序列化則可以使用一個迷你庫
            // 在最頂部引入`qs`,序列化提交的資料
            config.data = qs.stringify(config.data);
        }
        return config;
    },
    error => {
        message.error(error);
        Promise.reject(error);
    }
);

複製程式碼

問題七: Antd及moment預設全域性中文注入


import React, {
    StrictMode,
    unstable_ConcurrentMode as ConcurrentMode
} from 'react';
import ReactDOM from 'react-dom';
import App from 'App';

// mobx 注入
import { Provider } from 'mobx-react'; // react mobx的耦合器
import RootStore from 'store/store';

// 全域性中文
import { LocaleProvider } from 'antd';
import zh_CN from 'antd/lib/locale-provider/zh_CN';
import 'moment/locale/zh-cn';


ReactDOM.render(
    <Provider {...RootStore}>
        <LocaleProvider locale={zh_CN}>
            <App />
        </LocaleProvider>
    </Provider>,
    document.getElementById('root')
);

複製程式碼

總結

寫完了整個後臺管理系統,發現mobx並沒有想象中的好用;

看到阿里的umi已經2.x了(應該挺穩定了),準備用這個umi+dva重寫整個專案.

不對之處,請留言,會及時修正,謝謝閱讀

相關文章