本質上, 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的依賴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.js
。webpack
chunkFilename用來表示沒有在入口處的chunk文件打包後的名字,好比在index.js裏導入的外部的js文件。es6
path用來表示文件所有打包完之後存放的路徑文件夾位置,上述例子裏表示打包成功之後的文件都會存在一個叫作dist的文件夾裏。web
module模塊用來添加loader模塊來解析並轉換js文件,css文件,圖片等等,接下來根據不一樣的功能介紹一些經常使用的module模塊。
balel-loader用來轉換將es6轉換成es5代碼
@babel/core 轉換傳入的js代碼
@babel/preset-env用來兼容不一樣的瀏覽器,由於不一樣瀏覽器對es語法兼容性不一樣
@babel/preset-react用來對react語法進行轉換
@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-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-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')]
}
}
]
}
]
}
}複製代碼
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模塊爲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-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() //默認清除指定的打包後的文件夾
]
}複製代碼
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'
})
]
}複製代碼
用來提升開發效率,能夠用來設置熱更新,反向代理等功能
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設置後的頁面了
webpack在啓動後會在入口模塊處尋找全部依賴的模塊,resolve配置webpack如何去尋找這些模塊對應的文件
const config = {
resolve:{
exclude:['node_modules'], //去哪些地方尋找第三方模塊,默認是在node_modules下尋找
extensions:['js','jsx','json'], //當導入文件沒有帶後綴時,webpack會去自動尋找這種後綴的文件
alias:{
'@src':path.join(__dirname,'../src'), //將原導入路徑設置成新的路徑,就不須要每次導入時帶很長的斜槓了
}
}
}複製代碼
devtool 方便進行開發調試代碼
const config = {
devtool:'source-map'
}複製代碼
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-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')
)
複製代碼
在項目根目錄建立一個名字叫作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 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官方中文文檔