本质上, 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官方中文文档