手把手教你全家桶之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 条评论
登录 后参与评论

相关文章

来自专栏Golang语言社区

51. Socket服务端和客户端使用TCP协议通讯 | 厚土Go学习笔记

Socket服务器是网络服务中常用的服务器。使用 go 语言实现这个业务场景是很容易的。 这样的网络通讯,需要一个服务端和至少一个客户端。 我们计划构建一个这样...

3096
来自专栏腾讯NEXT学位

JavaScript全栈开发-工具篇(上)

? ? 一、运行环境 1. Node.js 2. Chrome 3. 其它浏览器 二、开发工具 1. WebStorm 1.1 功能特性 1.2 小技巧 1....

1241
来自专栏静默虚空的博客

Webpack 开发工具与模块热替换

Webpack 开发工具与模块热替换 ​⚠ 注意: 永远不要在生产环境中使用这些工具,永远不要。 devtool 当 JavaScript 异常抛出时,你常...

1886
来自专栏腾讯Bugly的专栏

H5 缓存机制浅析 移动端 Web 加载性能优化

1 H5 缓存机制介绍 H5,即 HTML5,是新一代的 HTML 标准,加入很多新的特性。离线存储(也可称为缓存机制)是其中一个非常重要的特性。H5 引入的离...

3182
来自专栏Python专栏

用python来更改小伙伴的windows开机密码,不给10块不给开机

2106
来自专栏琯琯博客

开发 Composer 包详细步骤

一、GitHub 创建一个名 uploadfile 新仓库,并克隆至本地。 二、初始化项目,生成composer.json文件 2.1 步骤 2.2 步骤解释...

61112
来自专栏张戈的专栏

WordPress前端html代码压缩优化,附对应知更鸟主题压缩报错的解决方案

今天,逛松果博客的时候,看到了他分享的《wordpress 通过代码压缩网页》一文,让我想起了其实张戈博客之前也用过这个功能,当时是在 WP 迷博客看到的纯代码...

3596
来自专栏三流程序员的挣扎

《图解HTTP》大纲

URI(Uniform Resource Identifier) 统一资源标识符;URL(Uniform Resource Locator) 统一资源定位符。

791
来自专栏FreeBuf

Xdigger Framework:一款网络安全测试辅助框架

*本文作者、Xdigger Framework开发者:guyoung Xdigger Framework是一款基于.Net Framework的网络安全测试辅助...

1877
来自专栏SDNLAB

脱坑神器,让你一步了解ODL控制器集群

一、控制器集群基本知识 1.1 Consensus一致性 Consensus一致性是指多个服务器在状态达成一致,但是在一个分布式系统中,因为各种意外可能,有的...

3937

扫码关注云+社区