從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官方中文文檔
相關文章
相關標籤/搜索