从0到1搭建webpack4.0+react全家桶(上)

webpack简介

本质上, webpack 是一个现代 JavaScript 应用程序的 静态模块打包器(module bundler) 。当 webpack 处理应用程序时,它会递归地构建一个 依赖关系图(dependency graph) ,其中包含应用程序须要的每一个模块,而后将全部这些模块打包成一个或多个 bundle


初始化项目

建立一个文件夹名字叫my-react-webpack javascript

而后进去文件夹 , npm init 之后一路回车就行。

mkdir my-react-webpack
cd my-react-webpack
npm init #一路回车复制代码

初始化完成之后就能够看到已经建立好一个package.json文件了,此时开始建立文件夹和文件。css

|--config
|--|--webpack.config.js
|--src
|--|--index.js
|--index.html复制代码

webpack模块

模式、入口、出口

先下载webpack的依赖html

cnpm install webpack webpack-cli webpack-dev-server -D
cnpm install path -D复制代码

在package.json添加下面两行前端

"script":{
    "build":"webpack --config ./config/webpack.config.js",
}复制代码

再来编写webpack.config.js文件java

const path = require('path');

const config = {
    mode:'production', //默认是两种模式 development production
    entry: {
        index: [path.resolve(__dirname,'../src/index.js')], //将index.js放入到入口处
    },
    output:{
        filename:'[name].[hash:8].js', //配置入口处的文件在打包后的名字,为了区分名字使用hash加密
        chunkFilename:'[name].[chunkhash:8].js', //配置无入口处的chunk文件在打包后的名字
        path:path.resolve(__dirname,'../dist')  //文件打包后存放的位置   
    }
};

module.exports = config;复制代码

mode:两种模式打包后会产生不一样的文件,development环境下打包后的文件是未压缩的js文件,而production环境下打包后的结果正相反。node

entry:能够放入多个须要打包的入口文件,也能够只放入一个index的入口文件,entry的值能够是一个字符路径  entry:path.resolve(__dirname,'../src/index.js') ,也能够是像我同样的是一个集合。react

output:filename来表示入口处的文件打包后的名字,上面filename的值里 name就是上面entry入口处的index,hash:8表示用hash进行加密而后生成8位随机字符,打包后的名字例如index.a0799b83.jswebpack

chunkFilename用来表示没有在入口处的chunk文件打包后的名字,好比在index.js里导入的外部的js文件。es6

path用来表示文件所有打包完之后存放的路径文件夹位置,上述例子里表示打包成功之后的文件都会存在一个叫作dist的文件夹里。web

module

module模块用来添加loader模块来解析并转换js文件,css文件,图片等等,接下来根据不一样的功能介绍一些经常使用的module模块。

处理js和jsx文件

balel-loader用来转换将es6转换成es5代码

@babel/core 转换传入的js代码

@babel/preset-env用来兼容不一样的浏览器,由于不一样浏览器对es语法兼容性不一样

@babel/preset-react用来对react语法进行转换

@babel/plugin-syntax-dynamic-import用来解析识别import( )的动态语法,并非转换

@babel/plugin-transform-runtime用来转换es6之后的新api,好比generator函数

cnpm install babel-loader @babel/core @babel/preset-env @babel/preset-react @babel/plugin-syntax-dynamic-import @babel/plugin-transform-runtime -D复制代码

const config = {
    module:{
        rules:[
            {
                test:/\.js[x]?$/, //匹配js或者jsx文件
                exclude:/node_modules/, //排除node依赖包的解析
                include: path.join(__dirname,'../src'), //针对src文件夹里的文件解析
                use:[{
                        loader:'babel-loader?cacheDirectory=true',
                        options:{
                            presets:['@babel/preset-env','@babel/preset-react'],
                            plugins:['@babel/plugin-syntax-dynamic-import',['@babel/plugin-transform-runtime']]
                        }
                 }]
            }
        ]
    }
}复制代码

处理css文件

css-loader用来加载css文件

style-loader使用<style>标签将css-loader内部样式注入到html里面

postcss-loader使用后借助autoprefixer能够自动添加兼容各类浏览器的css前缀

autoprefixer自动添加各类浏览器css前缀

cnpm install css-loader style-loader postcss-loader autoprefixer -D复制代码

const config = {
    module:{
        rules:[
            {
                test:/\.css$/,
                use:[
                    {
                        loader:'style-loader',
                    },{
                        loader:'css-loader',
                        options:{
                            modules:{
                                //建立css module防止css全局污染
                                localIndentName:'[local][name]-[hash:base64:4]' 
                            }
                        }
                    },{
                        loader:'postcss-loader',
                        options:{
                            plugins:[require('autoprefixer')]
                        }
                    }                
                ]
            }
           ]
    }
}复制代码

因为loader模块是从右向左解析的,因此须要先将各类浏览器的css前缀加上,再加载css样式,最后经过style标签添加到html里。

处理less文件

less 安装less服务

less-loader  解析打包less文件

cnpm install less less-loader -D复制代码

const config = {
    module:{
        rules:[
            {
                test:/\.less$/,
                use:[
                    {
                        loader:'style-loader',
                    },{
                        loader:'css-loader'
                    },{
                        loader:'less-loader',
                    },
                    {
                        loader:'postcss-loader',
                        options:{
                            plugins:[require('autoprefixer')]
                        }
                    }                
                ]
            }
           ]
    }
}复制代码

处理scss文件

node-sass 安装node解析sass的服务 (这里有一个less和sass的区别,less是基于JavaScript的在客户端处理,sass是基础ruby的因此在服务端处理 )

sass-loader 解析并打包sass,scss文件

cnpm intsall node-sass scss -D复制代码

const config = {
    module:{
        rules:[
            {
                test:/\.(sa|sc)ss$/,
                use:[
                    {
                        loader:'style-loader',
                    },{
                        loader:'css-loader'
                    },{
                        loader:'sass-loader',
                    },
                    {
                        loader:'postcss-loader',
                        options:{
                            plugins:[require('autoprefixer')]
                        }
                    }                
                ]
            }
        ]
    }
}复制代码

处理图片、音频等文件

url-loader 使用base64码加载文件,依赖于file-loader,能够设置limit属性当文件小于1m时使用file-loader

file-loader 直接加载文件

cnpm intsall url-loader file-loader --D复制代码

const config = {
    module:{
        rules:[
            {
                test:/\.(png|jpg|jpeg|gif)$/,
                use:{
                    loader:'url-loader',
                    options:{
                        limit:1024, //小于1m时使用url-loader
                        fallback:{
                            loader:'file-loader',
                            options:{
                                name:'img/[name].[hash:8].[ext]' //建立一个img的文件夹并将图片存入
                            }
                        }
                    }
                }
            },
            {
                test:/\.(mp4|mp3|webm|ogg|wav)$/,
                use:{
                    loader:'url-loader',
                    options:{
                        limit:1024,
                        fallback:{
                            loader:'file-loader',
                            options:{
                                name:'media/[name].[hash:8].[ext]'
                            }
                    }
            }
        }
     }
  ]
 }
}复制代码

plugins

plugins模块为webpack添加各类插件,用来扩展webpack的功能

部署不一样的环境

cross-env 指定webpack开启的mode模式   cnpm  install cross-env -D

修改package.json文件

"scripts":{
    "build": "cross-env NODE_ENV=production webpack --config ./config/webpack.config.js",
    "dev":"cross-env NODE_ENV=development webpack --config ./config/webpack.config.js"
}复制代码

修改webpack.config.js

const isDev = process.env.NODE_ENV === 'development' ? true : false;
const Webpack = require('webpack');

const config = {
      mode:isDev ? 'development':'production',
      plugins:[
        new Webpack.DefinePlugin({  //建立一个在编译时能够配置的全局变量
            'process.env':{
                NODE_ENV:isDev ? 'development':'production'
            }
        }),
        new Webpack.HotModuleReplacementPlugin() //webpack的热更新模块
]
}复制代码

添加html模板

html-webpack-plugin  为webpack建立一个html文件的模板

clean-webpack-plugin 下次打包时自动将上次已经打包完成的文件自动清除

cnpm install html-webpack-plugin clean-webpack-plugin -D复制代码

在项目根目录建立一个index.html文件的模板,而后在webpack.config.js里添加代码

const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');

const config = {
    plugins:[
        new HtmlWebpackPlugin({
            title:'主页', //生成html页面的标题
            filename:'index.html', //打包后的html文件名字
            template: path.join(__dirname,'../index.html'), //指令做为模板的html文件
            chunks:all, //当你须要将entry入口的多文件所有打包做为script标签引入时选择all
        }),
        new CleanWebpackPlugin() //默认清除指定的打包后的文件夹  
    ]
}复制代码

打包css文件

mini-css-extract-plugin 打包css、less、sass、scss文件

cnpm install mini-css-extract-plugin -D复制代码

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

const config={
    
    module:{rules:[
        //拿解析scss作为例子
        {
            test:'/\.(sa|sc)ss$/',
            use:[
                //开发环境不能用miniCssExtractPlugin解析,会报document未定义的错误
                isDev ? 'style-loader' : MiniCssExtractPlugin.loader, 
                {
                    loader:'css-loader',
                    options:{
                        modules:[
                             localIndentName:'[local][name]-[hash:base64:4]'
                        ]
                    }
                },{
                    loader:'sass-loader',
                },{
                    loader:'postcss-loader',
                    options:{
                        plugins:[require('autoprefixer')]
                    }
                }
            ]  
        }
    ]},
    plugins:[
        new MiniCssExtractPlugin({
            filename:'[name].[contenthash:8].css',
            chunkFilename:'[id].[contenthash:8].css'
            })
        ]
}复制代码

devServer

用来提升开发效率,能够用来设置热更新,反向代理等功能

const config = {
    devServer:{
        hot:true, //模块热更新
        contentBase: path.join(__dirname,'../dist'), //设置开启http服务的根目录
        historyApiFallback:true, //当路由命中一个路径后,默认返还一个html页面,解决白屏问题
        compress:true, //启动gzip压缩
        open:true, //启动完成后自动打开页面
        overlay:{
            error:true, //在浏览器全屏显示编译中的error
        },
        port:3000, //启动的端口号
        host:'localhost', //启动的ip
        /api/server:{
            target:'http://localhost:3010', //将/api/server的请求进行反向代理映射到3010端口上
            changeOrigin:true, //容许target是域名
            pathRewrite:{
                '^/api/server':'' //地址重写
            },
            //secure:false, //支持https代理
        }
    }
}复制代码

接下来修改package.json

"script":{
    "dev": "cross-env NODE_ENV=development webpack-dev-server --config ./config/webpack.config.js"
}复制代码

而后输入指令 npm run dev 就能成功启动devServer设置后的页面了

resolve

webpack在启动后会在入口模块处寻找全部依赖的模块,resolve配置webpack如何去寻找这些模块对应的文件

const config = {
    resolve:{
        exclude:['node_modules'], //去哪些地方寻找第三方模块,默认是在node_modules下寻找
        extensions:['js','jsx','json'], //当导入文件没有带后缀时,webpack会去自动寻找这种后缀的文件
        alias:{
            '@src':path.join(__dirname,'../src'), //将原导入路径设置成新的路径,就不须要每次导入时带很长的斜杠了
        }
    }
}复制代码

devtool

devtool 方便进行开发调试代码

  • source-map 源码调试时会产生一个source map文件,出错了会报出当前出错的行和列
  • inline-source-map 不会产生一个source map文件,但出错了会报出当前出错的行和列

const config = {
    devtool:'source-map'
}复制代码

webpack整体预览

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const Webpack = require('webpack');
const isDev = process.env.NODE_ENV === 'development';

const config = {
    mode: isDev ? 'development' : 'production',
    entry:{
        index:[path.resolve(__dirname,'../src/index.js')]
    },
    output:{
        filename:'[name].[hash:8].js',
        chunkFilename:'[name].[chunkhash:8].js',
        path:path.resolve('../dist')
    },
    module:{
        rules:[
            {
                test:/\.js[x]?$/,
                exclude: /node_modules/,
                include: path.join(__dirname,'../src'),
                use:[
                    {loader:'babel-loader?cacheDirectory=true',
                           options:{
                                presets:['@babel/preset-env','@babel/preset-react'],
                                plugins:['@babel/plugin-syntax-dynamic-import',[@babel/plugin-transform-runtime]]
                            }
                    }
                    ]
            },
            {
                test:/\.(sa|sc|c)ss$/,
                use:[
                    isDev ? 'style-loader' : MiniCssExtractPlugin.loader,
                    {
                        loader:'css-loader',
                        options:{
                            modules: {
                                localIndentName:'[local][name]-[hash:base64:4]'
                            }
                        }
                    },
                    {
                         loader:'sass-loader',
                    },
                    {
                        loader:'postcss-loader',
                        options:{
                            plugins:[require('autoprefixer')]
                        }
                    }
                ]
            },
            {
                test:/\.less$/,
                use:[
                    isDev ? 'style-loader' : MiniCssExtractPlugin.loader,
                    {
                        loader:'css-loader',
                        options:{
                            modules:{
                                localIndentName:'[local][name]-[hash:base64:4]'
                            }
                        }
                    },
                    {
                         loader:'less-loader',
                    },
                    {
                        loader:'postcss-loader',
                        options:{
                            plugins:[require('autopredixer')]
                        }
                    }
                ]
            },
            {
                 test:/\.(jpg|png|jpeg|gif)/,
                 use:[
                    {
                        loader:'url-loader',
                        options:{
                            limit:1024,
                            fallback:{
                                loader:'file-loader',
                                options:{
                                    name:'img/[name].[hash:8].[ext]'
                                }
                            }
                        }
                    }
                ]
            },
            {
                  test:/\.(mp3|mp4|webm|ogg|wav)/,
                  use:[{
                        loader:'url-loader',
                        options:{
                            limit:1024,
                            fallback:{
                                loader:'file-loader',
                                options:{
                                    name:'media/[name].[hash:8].[ext]'
                                    }
                                }
                        }
                     }]
            }
        ]
    },
    plugin:{
        new Webpack.DefinePlugin({
            process.env:{
                NODE_ENV: isDev ? 'development' : 'production'
           }
        }),
        new Webpack.HotModuleReplacementPlugin(),
        new HtmlWebpackPlugin({
            title:'主页',
            filename:'index.html',
            template: path.join(__diranme,'../index.html'),
            chunk:'all'
        }),
        new MiniCssExtractPlugin({
            filename:'[name].[contenthash:8].css',
            chunkFilename:'[id].[contenthash:8].css'
        }),
        new CleanWebpackPlugin()
    },
    resolve:{
        modules:['node_modules'],
        extensions:['jsx','js','json'],
        alias:{
            '@src':path.join(__dirname,'../src')
        }
    },
    devServer:{
        contentBase:path.join(__dirname,'../dist'),
        hot:true,
        compress:true,
        open:true,
        historyApiFallback:true,
        overlay:{
            error:true
        },
        host:'localhost',
        port:3000,
        proxy:{
        '/api/server':{
            target:'http:localhost:3010',
            pathRewrite:{
                '^/api/server':''
            },
            changeOrigin:true,
          //secure:false
         }
        }
    },
    devtool:'inline-source-map',
}

module.exports = config;复制代码

开启编写react

建立页面

先添加react和react-dom依赖包 cnpm install react react-dom --save

在index.js里添加代码

import React from 'react';
import ReactDom from 'react-dom';
import App from '@src/app';
ReactDom.render(
    <div>        
        <App />    
    </div>,    
    document.getElementById('app')
)复制代码

因为引用了app组件,因此在src文件夹下新建名字叫作app的文件夹,在app文件夹下面再建立index.jsx文件,如今来编写app组件

import React,{ Component } from 'react';
import styles from './style.scss';

export default class App extends Component {
    constructor(props) { 
        super(props);        
        this.state = {            
        data: 'hello'        
            }    
    }    
render() {
        return (
            <div className={styles.a}> 
               <p className={styles.b}> 
                   {this.state.data} 
               </p> 
           </div>        
        )    
}}复制代码

添加styles.scss验证css module和scss的编译是否成功

.a{
    .b{
          text-align:'center';
          color:'red';
      }
}复制代码

启动 npm run dev, 能够看到页面里一个居中并且是红色的hello文字,说明已经成功了


加入路由

添加react-router-dom和history模块 cnpm install react-router-dom history --save

在src文件夹下面再建立一个other的组件

import React, { Component } from 'react';

export default class Other extends Component {
    render() {
        return (
            <div> 
               other  
          </div>  
      ) 
   }}复制代码

再来修改index.js这个主函数

import React from 'react';
import ReactDom from 'react-dom';
import App from '@src/app';
import Other from '@src/other';
import { Router, Route, Switch } from 'react-router-dom';
import { createBrowserHistory } from 'history';
ReactDom.render(    
        <div>
            <Router history={createBrowserHistory()}>
            <Switch>                
                <Route path='/other' exact component={Other} />
                <Route path='/' component={App} />
            </Switch>
        </Router> 
   </div>,document.getElementById('app') 
)
复制代码

开启node后台服务

在项目根目录建立一个名字叫作server的文件夹,里面再放一个express应用,添加express和nodemon的模块 cnpm install express nodemon  body-parser --save

appServer.js

const express = require('express');
const bodyParser = require('body-parser');
const app = express();
const port = 3010;
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));

app.get('/get', (req, res) => {
    res.send(port + 'get请求成功')}
)

app.post('/post', (req, res) => {
    console.log(req.body)
    res.send(port + 'post请求成功, 前端传输数据为'+req.body)}
);

app.listen(port, () => {    
    console.log(port + '端口启动')}
)复制代码

在package.json里添加express启动监听代码

"script":{
    "express:dev" : "nodemon ./server/appServer.js"
}复制代码

加入redux通讯

添加redux的依赖 redux react-redux redux-thunk

在app文件夹里新建actionType.js、action.js、reducer.js三个文件

//actionType.js
export GET_DATA = 'app/getData';
export POST_DATA = 'app/postData';

//action.js
import * as actionTypes from './actionType.js'

export const getDataAction = () => {
    return (dispatch) => {
//因为webpack的devServer设置了反向代理,因此这里/api/server表明的是localhost:3010的node端口
        fetch('/api/server/get',{
            method:'GET',
            header:{
                'Content-Type':'application/json',
                 'Accept':'application/json,text/plain',
            }
        })
         .then(res => res.text())
         .then(obj => dispatch(getDataReducer(obj)) )
    }
}

const getDataReducer = (data) => ({
    type: actionTypes.GET_DATA,
    data
})
export const postDataAction = (meg) => {
    return (dispatch) => {
        fetch('/api/server/post',{
            method:'POST',
            header:{
                'Content-Type':'application/json',
                'Accept':'application/json,text/plain'    
            },
            body:JSON.Sringify({
                data:meg
            })
        })
         .then(res => res.text())
         .then(obj => dispatch(postDataReducer(obj)) )
    }
}

const postDataReducer = (data) => ({
    type: actionTypes.POST_DATA,
    data
})


//reducer.js
import * as actionTypes from './actionType.js'

export default (state={},action) => {
    switch(action.type){
        case actionTypes.GET_DATA:{
           return { ...state, getData:data}
        },
        case actionTypes.POST_DATA:{
           return { ...state, postData:data}
        },
        default: 
            return state;
    }
}复制代码

在src文件夹新建store.js来管理全部的数据

import { createStore, combinReducers, applyMiddleware, compose } from 'redux';
import { thunkMiddleware } from 'redux-thunk';
import  app_reducer from '@src/app/reducer';

const win = window;

const reducers = combinReducers({
    app:app_reducer,
})

const middlewares = [thunkMiddleware];

const storeEnhancers = compose(
    applyMiddleware(...middlewares),
    (win && win.__REDUX_DEVTOOLS_EXTENSION__) ? win.__REDUX_DEVTOOLS_EXTENSION__() : (f) => f,
);

const initState = {
    app:{
        getData:'',
        postData:''
    }
};

export default createStore(reducers,initState,storeEnhancers);复制代码

再来修改index.js

import React from 'react';
import ReactDom from 'react-dom';
import { Provider } from 'react-redux';
import store from '@src/store';
import App from '@src/app';
import Other from '@src/other';
import { Router, Route, Switch } from 'react-router-dom';
import { createBrowserHistory } from 'history';
ReactDom.render(    
        <div>
           <Provider store={store}>
             <Router history={createBrowserHistory()}>
                <Switch>                
                    <Route path='/other' exact component={Other} />
                    <Route path='/' component={App} />
                </Switch>
            </Router>
           </Provider> 
   </div>,document.getElementById('app') 
)复制代码

再回到app组件里来

import React,{ Component } from 'react';
import styles from './style.scss';
import { connect } from 'react-redux';
import * actions from './action';

class App extends Component {
    constructor(props) { 
        super(props);        
        this.state = {            
        data: 'hello'        
         }    
    }
    
    componentDidMount(){
        this.props.getDataFunc();
        this.props.postDataFunc(this.state.data);
    }
 
    render() {
    
        const {getData,postData} = this.props;
    
        return (
            <div className={styles.a}> 
               <p className={styles.b}> 
                   {this.state.data} 
               </p>
                <p>
                    {getData}
                </p>
                <p>
                    {postData}
                </p>
           </div>        
        )    
}}

const MapStateToProps = (state) =>({
    getData:state.app.getData,
    postData:state.app.postData
})

const MapDispatchToProps = (dispatch) => ({
    getDataFunc(){
        dipatch(actions.getDataRequest())
    },
    postDataFunc(meg){
        dispatch(actions.postDataRequest(meg))
    }
})

export connect(MapStateToProps,MapDispatchToProps)(App);复制代码

总结

从0开始写webpack+react的配置的确是有点难度的,可是写完之后感受又前进了一大步,并且收获也是很大的,帮助理解了原来不知道的底层打包的一些机制,等之后有空了再准备接着写下章webpack+react优化的方案。


若有错误或缺漏,欢迎指出。

参考文档

webpack.wuhaolin.cn/   webpack深刻浅出
www.webpackjs.com/    webpack官方中文文档
相关文章
相关标签/搜索