使用webpack已經有些年頭了,可是對於其中的一些基本配置仍是隻知其一;不知其二。爲了成爲一名優秀的webpack配置工程師,也是學習了一把webpack,react的配置,特分享這次經歷,並記錄當中遇到的一些問題。固然如今的配置只是很基礎的,但願在之後的工做經歷中,多多探索,把一些webpack優化,react,redux最佳實踐,都加入到其中。javascript
學習一個新技術,最好的獲取方式即是閱讀官方文檔。(https://www.webpackjs.com/gui...)。通讀之後,總結爲如下幾個要點。css
npm init -y npm install webpack webpack-cli --save-dev
// webpack.base.js const path = require('path'); module.exports = { entry: './src/index.js', output: { filename: '[name].bundle.js', path: path.resolve(__dirname, '../dist'), }, }; // package.json "scripts": { "dev": "webpack --config webpackconfig/webpack.base.js", }, // dist/index.html <!doctype html> <html> <head> <title>hyt</title> </head> <body> <script src="./main.bundle.js"></script> </body> </html> // src/index.js function component() { var element = document.createElement('div'); element.innerHTML = 'hello world hyt'; return element; } document.body.appendChild(component());
固然,避免咱們每次手動去清空dist文件下的內容,可使用clean-webpack-plugin插件幫助清空。html
npm install html-webpack-plugin clean-webpack-plugin // webpack.base.js const path = require('path'); const { CleanWebpackPlugin } = require('clean-webpack-plugin'); const HtmlWebpackPlugin = require('html-webpack-plugin'); module.exports = { entry: './src/index.js', output: { filename: '[name].bundle.js', path: path.resolve(__dirname, '../dist'), }, plugins: [ new CleanWebpackPlugin(), new HtmlWebpackPlugin({ title: 'Output Management' }) ], };
這裏能夠看到,HtmlWebpackPlugin已經幫助咱們生成了html文件。java
可是在平常開發中,每次修改完代碼都須要手動執行webpack打包命令,很繁瑣。這時候能夠採用 watch或者webpack-dev-server或者webpack-dev-middleware方法實現。較爲經常使用的是使用webpack-dev-server,不只提供一個簡單的 web 服務器,而且可以實時從新加載。node
npm install --save-dev webpack-dev-server const path = require("path"); const { CleanWebpackPlugin } = require("clean-webpack-plugin"); const HtmlWebpackPlugin = require("html-webpack-plugin"); module.exports = { entry: "./src/index.js", output: { filename: "[name].bundle.js", path: path.resolve(__dirname, "../dist"), }, devServer: { contentBase: './dist', open: true, port: 8888, }, plugins: [ new CleanWebpackPlugin(), new HtmlWebpackPlugin({ title: "Output Management", }), ], };
修改package.jsonreact
"scripts": { "dev": "webpack-dev-server --config webpackconfig/webpack.base.js", "watch": "webpack --config webpackconfig/webpack.base.js --watch" },
執行 npm run dev,看看效果。webpack
webpack.base.js
是通用配置,webapck.dev.js
中是開發環境配置,webapck.prod.js
是生產環境配置。webpack-merge
能夠幫住咱們很好的合併配置。nginx
接下來拆分配置:git
// webpack.base.js const path = require("path"); const { CleanWebpackPlugin } = require("clean-webpack-plugin"); const HtmlWebpackPlugin = require("html-webpack-plugin"); module.exports = { entry: "./src/index.js", output: { filename: "[name].bundle.js", path: path.resolve(__dirname, "../dist"), }, plugins: [ new CleanWebpackPlugin(), new HtmlWebpackPlugin({ title: "Output Management", }), ], };
// webpack.dev.js const { merge } = require("webpack-merge"); const base = require("./webpack.base"); module.exports = merge(base, { mode: "development", devtool: "inline-source-map", devServer: { contentBase: "./dist", open: true, port: 8888, }, });
const { merge } = require("webpack-merge"); const webpack = require("webpack"); const base = require("./webpack.base"); module.exports = merge(base, { mode: "production", devtool: "source-map", plugins: [ new webpack.DefinePlugin({ "process.env.NODE_ENV": JSON.stringify("production"), }), ], });
// package.json "scripts": { "dev": "webpack-dev-server --config webpackconfig/webpack.dev.js", "watch": "webpack --config webpackconfig/webpack.base.js --watch", "prod": "webpack --config webpackconfig/webpack.prod.js" },
到目前爲止,一個小型的webpack打包應用已經構建好了。接下來進入webpack應用中,引入react, css, less的處理。github
npm install react react-domm
修改src/index.js,改成react組件格式代碼。
import React from "react"; import ReactDOM from "react-dom"; const App = () => { return <div>hello world hyt</div>; }; ReactDOM.render(<App />, document.getElementById("root"));
由於react-dom的渲染節點,須要掛在已經存在的id=root節點上,因此咱們須要在生成的index.html中提早寫入 root節點。此操做能夠搭配以前提到的HtmlWebpackPlugin完成。添加template模板。
// src/template.html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title><%= htmlWebpackPlugin.options.title %></title> </head> <body> <div id="root"></div> </body> </html> // webpack.base.js new HtmlWebpackPlugin({ title: 'hyt Management', template: './src/template.html', }),
接下來運行,npm run dev,果真,報錯了。
提示咱們,應該須要專門的loader去處理咱們的js/jsx文件。這時候,就是大名鼎鼎的babel登場了。babel能夠幫助咱們進行js文件的編譯轉換。
除了幫助咱們對於高版本js語法轉換之外,還能夠處理react的jsx寫法。
npm install babel-loader @babel/preset-env @babel/preset-react @babel/core
更改webpack.base.js中rules規則。
module: { rules: [ { test: /\.(js|jsx)$/, exclude: /node_modules/, use: [{ loader: "babel-loader" }], }, ], },
根目錄新增.babelrc配置文件
{ "presets": ["@babel/preset-env", "@babel/preset-react"] }
接下來打包運行,npm run dev ,發現瀏覽器中終於顯示了<div>hello world hyt</div>
的dom(爲了顯示一行dom,咱們費了這麼大的功夫,不得不吐槽)。
有了剛纔js打包報錯的經驗,應該明白,要想加入css文件,也須要有專門的loader去處理css文件,得以運行。
npm install css-loader style-loader
css-loader處理css文件爲webpack可識別打包的,style-loader插入到頁面style中。
rules: [ { test: /\.(js|jsx)$/, exclude: /node_modules/, use: [{ loader: "babel-loader" }], }, { test: /\.css$/, use: [ { loader: "style-loader", }, { loader: "css-loader", }, ], }, ]
// src/index.js import "./style.css"; const App = () => { return <div className="hello">hello world hyt</div>; }; // src/style.css .hello { font-size: 30px; color: blue; }
嗯,能夠看到頁面中有顏色了。。
這時候思考一個問題,假如在咱們其餘組件中,也有一樣名字的class,再其對應的css文件中,寫了不一樣的樣式,會有什麼結果,實驗一下。
// src/components/about/index.js import React from "react"; import "./style.css"; const About = (props) => { return <div className="hello">About</div>; }; export default About;
// src/components/about/style.css .hello { color: red; }
// src/index.js import About from "./components/about"; <About />
看下頁面的展現,
發現color: red的樣式並無生效,打開控制檯看下打包後的樣式,名字同樣的class,樣式被覆蓋了。
因此這個時候,就引入css modules的概念了,經過css-loader的配置,幫助咱們實現css模塊化。
{ test: /\.css$/, use: [ { loader: "style-loader", }, { loader: "css-loader", options: { modules: { localIdentName: "[name]__[local]--[hash:base64:5]", // css-loader >= 3.x,localIdentName放在modules裏 }, }, }, ], }
更改js文件中引入方式。
import style from "./style.css"; const About = (props) => { return <div className={style["hello"]}>About</div>; }; index.js中同理
emm,樣式果真生效了
既然都用到css了,和不使用使用預處理less呢,可以更加提效咱們的開發。使用步驟和css大體相同,秩序多家less-loader先把less文件作一次轉換,再走css-loader的流程。大概配置以下
npm install less-loader { test: /\.less$/, use: [ { loader: "style-loader", // creates style nodes from JS strings }, { loader: "css-loader", // translates CSS into CommonJS options: { modules: { localIdentName: "[name]__[local]--[hash:base64:5]", // css-loader >= 3.x,localIdentName放在modules裏 https://github.com/rails/webpacker/issues/2197 }, }, }, { loader: "less-loader", // compiles Less to CSS options: { lessOptions: { javascriptEnabled: true },// less@3.x,須要開啓 配置項 javascriptEnabled: true }, }, ], },
把About中的css文件改成less使用便可。接下來能夠安心的寫代碼了。
爲了提升咱們的開發效率,在項目中引入antd組件庫。
兩種方法,全量引入css;或按需加載。(antd 4.x 的 JS 代碼默認支持基於 ES modules 的 tree shaking。)https://ant.design/docs/react...
採用按需加載的方法來構建項目。
npm install antd babel-plugin-import { "presets": ["@babel/preset-env", "@babel/preset-react"], "plugins": [ [ "import", { "libraryName": "antd", "libraryDirectory": "es", "style": true // `style: 'css'` 會加載 css 文件 } ] ] }
發現樣式並無加載成功。
緣由是咱們剛纔在處理less文件時,沒有區分src 和 node_modules,致使antd的class也加了modules,沒有加載到正確的樣式。修改less loader爲
{ test: /\.less$/, exclude: /node_modules/, // 這裏作了修改 use: [ { loader: "style-loader", // creates style nodes from JS strings }, { loader: "css-loader", // translates CSS into CommonJS options: { modules: { localIdentName: "[name]__[local]--[hash:base64:5]", // css-loader >= 3.x,localIdentName放在modules裏 https://github.com/rails/webpacker/issues/2197 }, }, }, { loader: "less-loader", // compiles Less to CSS options: { lessOptions: { javascriptEnabled: true }, }, }, ], }, { test: /\.less$/, include: /node_modules/, // 這裏作了修改 use: [ { loader: "style-loader", // creates style nodes from JS strings }, { loader: "css-loader", // translates CSS into CommonJS }, { loader: "less-loader", // compiles Less to CSS options: { lessOptions: { javascriptEnabled: true }, }, // less@3.x,須要開啓 配置項 javascriptEnabled: true, less-loader高版本須要lessOptions。 }, ], },
接下來引入React-Router實現單頁面應用。
具體用法可參考 https://reacttraining.com/rea...
npm install react-router-dom
修改index.js文件
import { BrowserRouter } from "react-router-dom"; import Routes from "./Routes"; const App = () => { return ( <BrowserRouter> <Routes /> </BrowserRouter> ); };
新建Routes.js
import React from "react"; import { Switch, Route, Link, Redirect } from "react-router-dom"; import About from "./components/about"; import User from "./components/user"; const Routes = () => { return ( <div> <nav> <ul> <li> <Link to="/about">About</Link> </li> <li> <Link to="/user">User</Link> </li> </ul> </nav> <Switch> <Route path="/about" component={About} /> <Route path="/User" component={User} /> <Redirect to="/about" /> </Switch> </div> ); }; export default Routes;
注意咱們使用的是BrowserRouter,本地開發webpack devserver須要開啓 historyApiFallback: true, 生產環境能夠在nginx端try_files。
單頁面應用ok了,接下來引入react-redux去管理咱們的數據流。
爲何選擇redux來管理咱們的數據流,以及redux的設計原理,能夠查看阮一峯老師的系列文章,這裏只給出基本使用。http://www.ruanyifeng.com/blo...
幾個比較重要的概念,Provider,connect, creatStore, reducer, applyMiddleware,actions。
繼續改造文件結構及內容
npm install redux react-redux
// src/store.js import { createStore } from "redux"; import reducers from "./reducers/index"; const store = createStore(reducers, {}); export default store;
// src/reducers/index.js import { combineReducers } from "redux"; const initialState = { name: "hyt", }; function home(state = initialState, action) { switch (action.type) { case "TEST_REDUCER": return { ...state, }; default: return state; } } export default combineReducers({ home, });
// src/index.js import { Provider } from "react-redux"; import Routes from "./Routes"; import store from "./store"; const App = () => { return ( <Provider store={store}> <BrowserRouter> <Routes /> </BrowserRouter> </Provider> ); };
新建容器組件container/home.js
import React from "react"; import { connect } from "react-redux"; const Home = (props) => { return <div>Home,{props.data.name}</div>; }; export default connect((state) => ({ data: state.home }))(Home);
import Home from "./containers/home"; const Routes = () => { return ( <div> <nav> <ul> ... <li> <Link to="/home">Home</Link> </li> </ul> </nav> <Switch> ... <Route path="/home" component={Home} /> <Redirect to="/about" /> </Switch> </div> ); };
這是路由localhost:8080/home下就能夠顯示出 hello,hyt的數據。
上面已經獲取到了store中的數據,接下來dispatch去改變store中的數據,因爲組件訂閱了store(connect),頁面數據源會自動渲染變動。
6.1 添加action types常量
// src/constants/actionTypes.js export const SET_USER_NAME = "SET_USER_NAME";
6.2 改變store的action
// src/actions/homeAction.js import { SET_USER_NAME } from "../constants/actionsType"; export function setName(payload) { return { type: SET_USER_NAME, payload }; }
6.3 接受actions的reducer
// src/reducers/index.js import { SET_USER_NAME } from "../constants/actionsType"; const initialState = { name: "hyt", }; function home(state = initialState, action) { switch (action.type) { case SET_USER_NAME: return { ...state, name: action.payload.name, }; default: return state; } }
6.4 組件觸發actions。增長了mapDispatchToProps。props.setName()
// src/containers/home.js import React, { useEffect } from "react"; import { connect } from "react-redux"; import { setName } from "../actions/homeAction"; const Home = (props) => { useEffect(() => { setTimeout(() => { props.setName({ name: "wjh", }); }, 3000); }, []); return <div>Home,{props.data.name}</div>; }; const mapDispatchToProps = { setName, }; export default connect( (state) => ({ data: state.home }), mapDispatchToProps )(Home);
如今頁面中的,hello,hyt 會在三秒後變成 hello,wjh。
六. redux中間件,thunk/saga
如今咱們處理的是同步數據,接下來咱們引入redux中間件,去處理異步action函數。
修改store,
npm install redux-thunk
// src/store.js import { createStore, applyMiddleware } from "redux"; import thunk from "redux-thunk"; import reducers from "./reducers/index"; const store = createStore(reducers, {}, applyMiddleware(thunk)); export default store;
// src/actions/homeAction.js export function getName(payload) { return (dispatch) => { return Promise.resolve().then((res) => { dispatch({ type: SET_USER_NAME, payload: { name: "fetch mock", }, }); return res; }); }; }
// src/containers/home.js const Home = (props) => { useEffect(() => { setTimeout(() => { // props.setName({ // name: "wjh", // }); props.getName(); }, 3000); }, []); return <div>Home,{props.data.name}</div>; }; const mapDispatchToProps = { setName, getName, };
頁面上已經變成了 hello,fetch mock.
saga的使用能夠直接參考 https://github.com/hytStart/J...
npm install react-loadable
修改Routes中組件引入方式,達到按路由拆分
js模塊
import Loadable from "react-loadable"; const MyLoadingComponent = (props) => { if (props.pastDelay) { return <div>Loading...</div>; } return null; }; const User = Loadable({ loader: () => import("./components/user"), loading: MyLoadingComponent, delay: 300, });
能夠看到控制檯js bundle加載。
因爲如今咱們每改一下代碼,均可以看到刷新一次頁面,因而以前的路由跳轉狀態、表單中填入的數據都會重置。對於開發人員過程很不方便,這時候就引出咱們的熱更新了,不會形成頁面刷新,而是進行模塊的替換。
// webpack.dev.js module.exports = merge(base, { mode: "development", devtool: "inline-source-map", devServer: { contentBase: "./dist", open: true, port: 8888, historyApiFallback: true, hot: true, // +++++++ }, });
// index.js const App = () => { return ( <Provider store={store}> <BrowserRouter> <Routes /> </BrowserRouter> </Provider> ); }; ++++ if (module.hot) { module.hot.accept(); } ++++ ReactDOM.render(<App />, document.getElementById("root"));
如今咱們的項目中尚未專門的loader去處理圖片,
file-loader 能夠指定要複製和放置資源文件的位置,以及如何使用版本哈希命名以得到更好的緩存。此外,這意味着 你能夠就近管理圖片文件,可使用相對路徑而不用擔憂部署時 URL 的問題。使用正確的配置,webpack 將會在打包輸出中自動重寫文件路徑爲正確的 URL。
url-loader 容許你有條件地將文件轉換爲內聯的 base-64 URL (當文件小於給定的閾值),這會減小小文件的 HTTP 請求數。若是文件大於該閾值,會自動的交給 file-loader 處理。
增長以下配置
npm install file-loader url-loader // webpack.base.js { test: /\.(mp4|ogg)$/, use: [ { loader: 'file-loader', }, ], }, { test: /\.(png|jpg|jpeg|gif|eot|svg|ttf|woff|woff2)$/, use: [ { loader: 'url-loader', options: { limit: 8192, }, }, ], },
該插件將CSS提取到單獨的文件中。它爲每一個包含CSS的JS文件建立一個CSS文件。它支持CSS和SourceMap的按需加載。
4.1 使用mini-css-extract-plugin
npm install --save-dev mini-css-extract-plugin
修改webpack.base.js中關於css,
less的配置,替換掉style-loader(不在須要把style插入到html中,而是經過link引入)。
{ test: /\.css$/, use: [ // { // loader: "style-loader", // }, { loader: MiniCssExtractPlugin.loader, options: { esModule: true, hmr: process.env.NODE_ENV === "dev", reloadAll: true, }, }, { loader: "css-loader", options: { modules: { localIdentName: "[name]__[local]--[hash:base64:5]", // css-loader >= 3.x,localIdentName放在modules裏 https://github.com/rails/webpacker/issues/2197 }, }, }, ], }, { test: /\.less$/, exclude: /node_modules/, use: [ // { // loader: "style-loader", // creates style nodes from JS strings // }, { loader: MiniCssExtractPlugin.loader, options: { esModule: true, hmr: process.env.NODE_ENV === "dev", reloadAll: true, }, }, { loader: "css-loader", // translates CSS into CommonJS options: { modules: { localIdentName: "[name]__[local]--[hash:base64:5]", // css-loader >= 3.x,localIdentName放在modules裏 https://github.com/rails/webpacker/issues/2197 }, }, }, { loader: "less-loader", // compiles Less to CSS options: { lessOptions: { javascriptEnabled: true }, }, }, ], }, { test: /\.less$/, include: /node_modules/, use: [ // { // loader: "style-loader", // creates style nodes from JS strings // }, { loader: MiniCssExtractPlugin.loader, options: { esModule: true, hmr: process.env.NODE_ENV === "dev", reloadAll: true, }, }, { loader: "css-loader", // translates CSS into CommonJS }, { loader: "less-loader", // compiles Less to CSS options: { lessOptions: { javascriptEnabled: true }, }, // less@3.x,須要開啓 配置項 javascriptEnabled: true, less-loader高版本須要lessOptions。 }, ], },
4.2 如上配置,增長hrm配置
hmr: process.env.NODE_ENV === "dev"
同時在package.json scripts中注入環境變量
"scripts": { "dev": "NODE_ENV=dev webpack-dev-server --config webpackconfig/webpack.dev.js", "watch": "NODE_ENV=dev webpack --config webpackconfig/webpack.base.js --watch", "prod": "webpack --config webpackconfig/webpack.prod.js" },
4.3 plugin配置
plugins: [ new CleanWebpackPlugin(), new HtmlWebpackPlugin({ title: "Output Management", template: "./src/template.html", }), new MiniCssExtractPlugin({ // Options similar to the same options in webpackOptions.output // both options are optional filename: "[name].css", chunkFilename: "[id].css", }), ],
到目前爲止,咱們已經根據引入文件的方式,分離除了css,作到了按需加載。可是如今能夠查看打包出來的css文件是沒有通過壓縮的。
4.4 增長optimize-css-assets-webpack-plugin來壓縮css代碼,可是這時又會出現另一個問題,optimization.minimizer會覆蓋webpack提供的默認設置,所以還需增長terser-webpack-plugin來壓縮js代碼。
npm install --save-dev optimize-css-assets-webpack-plugin terser-webpack-plugin
// webapack.base.js const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin"); const TerserJSPlugin = require("terser-webpack-plugin"); plugins: [ new CleanWebpackPlugin(), new HtmlWebpackPlugin({ title: "Output Management", template: "./src/template.html", }), new MiniCssExtractPlugin({ // Options similar to the same options in webpackOptions.output // both options are optional filename: "[name].css", chunkFilename: "[id].css", }), ], optimization: { minimizer: [new TerserJSPlugin({}), new OptimizeCSSAssetsPlugin({})], },
https://webpack.docschina.org...
mode: 'production'