• 手把手教你全家桶之React(三)--完结篇


    前言

    本篇主要是讲一些全家桶的优化与完善,基础功能上一篇已经讲得差不多了。直接开始:

    Source Maps

    当javaScript抛出异常时,我们会很想知道它发生在哪个文件的哪一行。但是webpack 总是将文件输出为一个或多个bundle,我们对错误的追踪很不方便。Source maps试图解决这一个问题,我们只需要改变一下配置项即可。
    在webpack.dev.config.js中加入:

    devtool:"inline-source-map"
    

    css编译

    • 这里以less-loader为例,先安装
    1. less-loader 是组件中可以引入less后缀的文件
    2. css-loader 是使css文件可以用@import和url(...)的方法实现require;
    3. style-loader 使计算后的样式加入到页面中。
    npm install --save-dev less-loader less css-loader style-loader
    
    • 配置webpack.dev.config.js文件
     module:{
            rules:[
                {
                    test:/.js$/,
                    use:['babel-loader?cacheDirectory=true'],
                    include:path.join(__dirname,'src')
                },{
                    test:/.less$/,
                    use:[
                        'style-loader',
                        {loader:'css-loader',options:{importLoaders:1}},
                        'less-loader'
                    ]
                }
            ]
        },
    

    测试下

    cd src/pages/Home
    touch Home.less
    

    打开 Home.less

    .wrap{
        300px;
        height:300px;
        background:red;
        & .content{
            200px;
            height:200px;
            margin:auto;
            background:yellow;
        } 
     }
    

    在Home.js中引入,并添加class

    import './Home.less'
    ...
      render(){
            return(
                <div>
                    <h1>当前共点击次数为:{this.state.count}</h1>
                    <button onClick={()=> this._test()}>点击我!</button>
                    <div className="wrap">
                        <div className="content"></div>
                    </div>
                </div>
            )
        }
    

    因为添加了新的依赖,我们重新跑一次npm run start,效果如图

    图片编译

    先进行一个测试,打开src/Pages/UserInfo/UserInfo.js

    import imgSrc from '../../../public/image/react15.png'
        ...
        <h2>个人资料</h2>
        <img src={imgSrc}/>
    

    运行后,页面报错

    出现这个错误是因为打包后的文件找不到我们之前写好的相对路径。对此,我们可以用如下方式解决。
    首先我们要安装两个依赖:

    • file-loader 当我们写样式比如背景图片,我们的路径是相对于当前文件的,但webpack最终会打包成一个文件。打包后的相对路径会找不到对应文件。这时,file-loader可以帮我们找到正确的文件路径。
    • url-loader 如果图片过多,会增加过多的http请求,url-loader提示图片base64编码服务,设定limit参数,小于设置值的图片会被转为一串字符,只需将字符打包到文件中,就能访问图片了。
    npm install --save-dev url-loader file-loader
    

    在webpack.dev.config.js增加配置

    module:{
            rules:[
                ...
                {
                    test:/.(png|jpg|gif)$/,
                    use:[{
                        loader:'url-loader',
                        options:{
                            // 设置为小于8K的大小
                            limit:8192
                        }
                    }]
                }
            ]
    }
    

    配置成功后,我们重新运行npm run start(因为新加了依赖要重新跑一次服务),看下效果(PS:盗用大幂幂的照片_

    按需加载

    我们打包后,页面统一生成bundle.js,当我们进入Home页面时,因为加载的文件过多会导致页面慢。我们想要达到跳转到对应页面时按需加载文件的效果,就需要用到bundle-loader。

    • 安装
    npm install bundle-loader --save
    
    • 在router下新建Bundle.js
    cd src/router
    touch Bundle.js
    

    打开Bundle.js,根据示例

    import React,{Component} from 'react'
    class Bundle extends Component{
        state={
            mod:null
        };
        componentWillMount(){
            this.load(this.props)
        }
        componentWillReceiveProps(nextProps){
            if(nextProps.load !== this.props.load){
                this.load(nextProps)
            }
        }
        load(props){
            this.setState({
                mod:null
            });
            props.load((mod)=>{
                this.setState({
                    mod:mod.default ? mod.default : mod
                })
            })
        }
        render(){
            return this.props.children(this.state.mod)
        }
    }
    export default Bundle;
    
    • 路由配置改造,src/router/router.js
    import React from 'react';
    import {BrowserRouter as Router,Route,Switch,Link} from 'react-router-dom';
    
    import Home from 'bundle-loader?lazy&name=home!pages/Home/Home';
    import About from 'bundle-loader?lazy&name=page1!pages/About/About';
    import Counter from 'bundle-loader?lazy&name=counter!pages/Counter/Counter';
    import UserInfo from 'bundle-loader?lazy&name=userInfo!pages/UserInfo/UserInfo';
    const Loading = function(){
        return <div>Loading...</div>
    };
    const createComponent = (component) => (props) => (
        <Bundle load={component}>
            {
                (Componet) => Component ? <Component {...props} /> : <Loading/>
            }
        </Bundle>
    );
    const getRouter=()=>(
        <Router>
            <div>
                <ul>
                    <li><Link to="/">Home</Link></li>
                    <li><Link to="/about">About</Link></li>
                    <li><Link to="counter">Counter</Link></li>
                    <li><Link to="userinfo">UserInfo</Link></li>
                </ul>
            
                <Switch>
                    <Route exact path="/" component={createComponent(Home)}/>
                    <Route path="/about" component={createComponent(About)}/>
                    <Route path="/counter" component={createComponent(Counter)}/>
                    <Route path="/userinfo" component={createComponent(UserInfo)}/>
                </Switch>
            </div>
        </Router>
    
    );
    export default getRouter;
    
    • 修改webpack.dev.config.js配置,使打包输出的文件名对应
    output:{
        path:path.join(__dirname,'./dist'),
        filename:'bundle.js',
        chunkFilename:'[name].js'
    }
    

    运行npm run start 效果如图

    缓存

    按需加载文件的进阶优化则是文件缓存。缓存我们要解决以下两个问题:

    1. 当用户首次访问Home.js时,进行文件的加载,第二次访问时再进行同样文件的加载吗?
    2. 当文件做了缓存时,我们如果有改动代码,重新打包,我们要如何更新缓存的文件?
      问题1在浏览器中已经对静态资源文件做了缓存,我们主要解决问题二。
      日常开发中,我们是通过打包修改文件名(比如加hash),使客户端能识别新的文件,重新加载。
      打开webpack.dev.config.js
    output:{
        path:path.join(__dirname,'./dist'),
        filename:'[name].[hash].js',
        chunkFilename:'[name].[chunkhash].js'
    }
    

    我们可以看到编译后的文件名已经变了

    由于我们在dist/index.html中引用的还是bundle.js,所以我们要改成每次编译后自动插入到index.html中,可以用到HtmlWebpackPlugin。

    • 安装
    npm install html-webpack-plugin --save-dev
    
    • 新建入口模板文件index.html
    cd src
    touch index.html
    
    • 打开index.html
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <meta http-equiv="X-UA-Compatible" content="ie=edge">
        <title>Document</title>
    </head>
    <body>
        <div id="app"></div>
    </body>
    </html>
    
    • 修改webpack.dev.config.js配置文件
    var HtmlWebpackPlugin=require('html-webpack-plugin');
    ...
    plugins:[new HtmlWebpackPlugin({
        filename:'index.html',
        template:path.join(__dirname,'src/index.html')
    })],
    

    此时删掉之前的dist/index.html,运行npm run start访问正常。

    公共代码提取

    我们打包生成的文件js文件中,都包含了react,redux,react-router这样的代码。然而这些依赖代码我们在很多文件都引用了,而不需要它自动更新。所以我们可以把这些公共代码提取出来。
    我们根据教程配置。

    • 打开webpack.dev.config.js
    var webpack=require('webpack');
    module.exports={
        entry:{
            app:[
                'react-hot-loader/patch',
                path.join(__dirname,'src/index.js')
            ],
            vendor:['react','react-router-dom','redux','react-dom','react-redux']
        },
        plugins:[
            ...
            new webpack.optimize.CommonsChunkPlugin({
                name:'vendor'
            })
        ]
    }
    

    重新运行,打包文件如下

    可以发现app.[hash].js和vendor.[hash].js生成的hash是一样的。也就意味着如果代码有改动app.[hash].js与vendor.[hash].js都会同时改变。然后vendor里的内容我们不希望它更新。根据文档,我要在webpack里还要配置

    应用到我们项目应该

    output:{
        path:path.join(__dirname,'./dist'),
        filename:'[name].[chunkhash].js',
        chunkFilename:'[name].[chunkhash].js'
    }
    

    再次运行,发现报错,webpack-dev-server --hot 不兼容chunkhash

    解决这个问题,我们要先区分生产环境与开发环境的区别。所以,上面的问题先留一下,我们先来构建生产环境的配置。

    生产环境构建

    生产环境与开发环境的区别往往体现在目标差异大。开发环境我们要配置的东西很多,要求实时加裁,热更新模块等。但生产环境要求较小,更关注小的bundle,更轻量的Source map,更高效的加载时间等。

    • 首先创建配置文件
    touch webpack.config.js
    
    • 将之前webpack.dev.config.js的内容复制到webpack.config.js中,删除一些和开发环境有关的几点:
    1. webpack-dev-server相关内容
    2. devtool的值改成 cheap-module-source-map
    3. 输出文件名增加字符改为chunkhash,原本的webpack.dev.config.js改回为hash
      根据以上几点,webpack.config.js内容如下:
    var path=require('path');
    var HtmlWebpackPlugin=require('html-webpack-plugin');
    var webpack=require('webpack');
    module.exports={
        // 入口文件指向src/index.js
        entry:{
            app:[
                'react-hot-loader/patch',
                path.join(__dirname,'src/index.js')
            ],
            vendor:['react','react-router-dom','redux','react-dom','react-redux']
        },
        //打包后的文件到当前目录下的dist文件夹,名为bundle.js 
        output:{
            path:path.join(__dirname,'./dist'),
            filename:'[name].[chunkhash].js',
            chunkFilename:'[name].[chunkhash].js'
        },
        module:{
            rules:[
                {
                    test:/.js$/,
                    use:['babel-loader?cacheDirectory=true'],
                    include:path.join(__dirname,'src')
                },{
                    test:/.less$/,
                    use:[
                        'style-loader',
                        {loader:'css-loader',options:{importLoaders:1}},
                        { loader: 'less-loader', options: { strictMath: true, noIeCompat: true } }
                    ]
                },
                {
                    test:/.(png|jpg|gif)$/,
                    use:[{
                        loader:'url-loader',
                        options:{
                            limit:8192
                        }
                    }]
                }
    
            ]
        },
        plugins:[
            new HtmlWebpackPlugin({
                filename:'index.html',
                template:path.join(__dirname,'src/index.html')
            }),
            new webpack.optimize.CommonsChunkPlugin({
                name:'vendor'
            })
        ],
        devtool:"cheap-module-source-map",
        resolve:{
            alias:{
                pages:path.join(__dirname,'src/pages'),
                components:path.join(__dirname,'src/components'),
                router:path.join(__dirname,'src/router'),
                actions:path.join(__dirname,'src/redux/actions'),
                reducers:path.join(__dirname,'src/redux/reducers'),
                // redux:path.join(__dirname,'src/redux') 与模块重名
            }
        }
    };
    
    • 在package.json中增加build打包命令,指定配置文件。
     "scripts": {
        "test": "echo "Error: no test specified" && exit 1",
        "build": "webpack --config webpack.config.js",
        "start": "webpack-dev-server --config webpack.dev.config.js --color --progress --hot"
      },
    

    运行一次打包命令 npm run build,文件名支持了chunkhash.

    虽然文件名不同了,但是改变代码重新打包会发现app.[hash].js和vendor.[chunkhash].js一样都更新了名字,这不就和没拆分是一样的吗?
    别着急,看官网介绍

    注意mainfest与vendor的顺序不能错哦

    • 打开webpack.config.js
     plugins:[
            new HtmlWebpackPlugin({
                filename:'index.html',
                template:path.join(__dirname,'src/index.html')
            }),
            new webpack.HashedModuleIdsPlugin(),
            new webpack.optimize.CommonsChunkPlugin({
                name:'vendor'
            }),
            new webpack.optimize.CommonsChunkPlugin({
                name:'mainfest'
            })   
        ]
    

    当我们构建了基础的生产环境配置后,我们可以增加指定环境配置,根据process.env.NODE_ENV环境变量关联,让library中应该引用哪些内容。例如,当不处于生产环境中时,library可能会添加额外的日志log和test。当使用 process.env.NODE_ENV === 'production' 时,一些 library 可能针对具体用户的环境进行代码优化,从而删除或添加一些重要代码。

    • 打开webpack.config.js
    module.exports={
        plugins:[
            ...
            new webpack.DefinePlugin({
                'process.env':{
                    'NODE_ENV':JSON.stringify('production')
                }
            })
        ]
    }
    

    打包优化

    文件压缩

    webpack使用UglifyJSPlugin来压缩打包后生成的文件。

    • 安装
    npm install uglifyjs-webpack-plugin --save-dev
    
    • 打开webpack.config.js进行配置
    const UglifyJSPlugin=require('uglifyjs-webpack-plugin')
    module.exports={
        plugins:[
            ...
            new UglifyJSPlugin()
        ]
    }
    

    运行npm run build有没有发现打包的文件小了好多

    清理dist文件

    每次打包dist都会多好多文件混合在里面,我们应该清掉之前打包的文件,只留下当前打包后的文件。我们用到clean-webpack-plugin

    • 安装
    npm install clean-webpack-plugin --save-dev
    
    • 打开webpack.config.js来配置
    const CleanWebpackPlugin=require('clean-webpack-plugin');
    ...
    plugins:[
        new CleanWebpackPlugin(['dist'])
    ]
    

    现在试试打包一下,每次是不是都是直接覆盖整个文件。虽然api文件也被清掉了,但是没关系,那只是用来测试的。

    静态文件的基本路径

    当我们打包后,静态文件没办法定位到静态服务器,我们需要在webpack.config.js中配置

    output:{
        ...
        publicPath:'/'
    }
    

    css打包分离

    如果我要要将打包到js的css内容抽出来单独成css文件,我们可以使用extract-text-webpack-plugin.

    • 安装
    npm install extract-text-webpack-plugin --save-dev
    
    • 打开webpack.config.js进行配置
    const ExtractTextPlugin=require("extract-text-webpack-plugin");
    module.exports={
        module:{
            rules:[
                ...
                {
                    test:/.(css|less)$/,
                    use:ExtractTextPlugin.extract({
                        fallback:"style-loader",
                        use:"css-loader"
                    })
                }
            ]
        },
        plugins:[
            ...
            new ExtractTextPlugin({
                filename:'[name].[contenthash:5].css',
                allChunks:true
            })
        ]
    }
    

    我们可以增加一些css文件引用,来测试下。由于我们之前的示例是用less来写的样式,那么我们加上less的配置,使之生成独立文件。
    修改刚刚的配置项:

    module.exports={
        module:{
            rules:[
                ...
                {
                    test:/.(css|less)$/,
                    use:ExtractTextPlugin.extract({
                        fallback:"style-loader",
                        use:["css-loader","less-loader"]
                    })
                }
            ]
        },
    }
    

    重新打包,就能看到被生成的css文件啦

    axios

    • 安装axios
    npm install --save axios
    
    • 然后简化之前写的userInfo的action,修改redux/actions/userInfo.js
    export const GET_USERINFO_REQUEST="userInfo/GET_USERINFO_REQUEST";
    export const GET_USERINFO_SUCCESS="userInfo/GET_USERINFO_SUCCESS";
    export const GET_USERINFO_FAIL="userInfo/GET_USERINFO_FAIL";
    
    export function getUserInfo(){
        return{
            types:[GET_USERINFO_REQUEST,GET_USERINFO_SUCCESS,GET_USERINFO_FAIL],
            promise:client => client.get('/api/userInfo.json')     
        }
    }
    

    其中dispath(getUserInfo())后,是通过redux的中间件来处理的。为了弄清楚,我们自己来写一个。

    自定义Middleware

    • 清理逻辑
    1. 发起请求前 dispatch REQUEST;
    2. 请求成功后 dispatch SUCESS,再执行callback;
    3. 请求失败后 dispatch FAIL。
    • 创建基本文件
    cd src/redux
    mkdir middleware && cd middleware
    touch promiseMiddleware.js
    
    • 定义promiseMiddleware.js的内容
    import axios from 'axios';
    export default store => next =>action =>{
        const {dispatch,getState}=store;
        // 如果dispatch传来的是一个function,则跳过
        if(typeof action === 'function'){
            action(dispatch,getState);
            return ;
        }
        // 解析action
        const {
            promise,
            types,
            afterSuccess,
            ...rest
        }=action;
        // 如果不是异步请求则直接跳转下一步
        if(!action.promise){
            return next(action);
        }
        // 解析types
        const [REQUEST,SUCCESS,FAILURE]=types;
        // 发送action
        next({
            ...rest,
            type:REQUEST
        });
        // 成功
        const onFulfilled = result=>{
            next({
                ...rest,
                result,
                type:SUCCESS
            });
            if(afterSuccess){
                afterSuccess(dispatch,getState,result);
            }
        };
        // 失败
        const onRejected=error=>{
            next({
                ...rest,
                error,
                type:FAILURE
            });
        };
        return promise(axios).then(onFulfilled,onRejected).catch(error=>{
            console.error('MIDDLEWARE ERROR:',error);
            onRejected(error)
        })
    }
    
    • 在src/redux/store.js中应用中间件
    import {createStore,applyMiddleware} from 'redux';
    import combineReducers from './reducers.js';
    // import thunkMiddleware from 'redux-thunk';
    // let store = createStore(combineReducers,applyMiddleware(thunkMiddleware));
    
    import promiseMiddleware from './middleware/promiseMiddleware';
    let store = createStore(combineReducers,applyMiddleware(promiseMiddleware));
    
    export default store;
    
    • 最后修改src/redux/reducers/userInfo.js
      因为是当action请求成功,我们在中间件会自动加上一个result字段来存结果。
    export default function reducer(state=initState,action){
        switch(action.type){
            ...
            case GET_USERINFO_SUCCESS: 
                return{
                    ...state,
                    isLoading:false,
                    userInfo:action.result.data,
                    errMsg:''
                }
        }
    }
    

    我们重启npm run start ,访问userInfo接口是不是成功啦!
    好啦,先写到这吧,如果还有细节完善会在源码上更新。源码地址,欢迎star和issues。

  • 相关阅读:
    匈牙利算法-二分图的最大匹配
    UOJ 407(IOI2018 D1T3)
    UOJ 460
    UOJ 405(IOI2018 D1T1)
    Codeforces 1110E
    2.文件结构
    1.常用快捷键
    Python3.x和Python2.x的差异
    javascript 常用内置对象
    94. Binary Tree Inorder Traversal(非递归实现二叉树的中序遍历)
  • 原文地址:https://www.cnblogs.com/zhoulin-circle/p/9115289.html
Copyright © 2020-2023  润新知