手把手教你全家桶之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{ width:300px; height:300px; background:red; & .content{ width: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

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。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Netkiller

PHP 安全与性能

目录 1. Apache mod_php / php-fpm 1.1.1. Apache 1.1.2. Nginx / lighttpd + fastcgi 1...

2555
来自专栏Netkiller

PHP 安全与性能

PHP 安全与性能 摘要 我的系列文档 Netkiller Architect 手札Netkiller Developer 手札Netkiller PHP 手札...

3686
来自专栏有趣的django

Django REST framework+Vue 打造生鲜超市(六) 七、用户登录与手机注册

七、用户登录与手机注册 7.1.drf的token (1)INSTALL_APP中添加 INSTALLED_APPS = ( ... 'rest...

1K8
来自专栏Ryan Miao

Linux 学习笔记

Linux学习笔记 请切换web视图查看,表格比较大,方法:视图》》web板式视图 博客园不能粘贴图片吗  http://wenku.baidu.com/vie...

3338
来自专栏Lambda

TortoiseSVN文件夹及文件图标不显示解决方法 TortoiseSVN文件夹及文件图标不显示解决方法

TortoiseSVN文件夹及文件图标不显示解决方法           由于自己的电脑是win7(64位)的,系统安装TortoiseSVN之后,其他的功...

2008
来自专栏openshift持续集成

jenkins邮件插件中的内容参数设置

众所周知,Jenkins默认提供了一个邮件通知,能在构建失败、构建不稳定等状态后发送邮件。但是它本身有很多局限性,比如它的邮件通知无法提供详细的邮件内容、无法定...

5958
来自专栏進无尽的文章

扒虫篇-Bug日志 Ⅱ

事情是这样的:一个风和日丽的下午,我正在 itunesConnect 中注册一个APP,基本信息都保存了,在编辑版本信息时,都弄的差不多了,可是没有保存,结果不...

571
来自专栏张善友的专栏

基于WAS 部署WCF服务

Windows (Process) Activation Service (WAS)作为 IIS7.0 特有的新增功能,和以前IIS 6.0的功能相比更加强大,...

20210
来自专栏为数不多的Android技巧

简化markdown写作中的贴图流程

这么复杂的流程,让人简直没有了插入图片的欲望;但是大量的文字没有图片,必然让人疲惫;

1015
来自专栏Netkiller

PHP 安全与性能

PHP 安全与性能 摘要 我的系列文档 Netkiller Architect 手札Netkiller Developer 手札Netkiller PHP 手札...

2675

扫码关注云+社区