从零开始webpack搭建react,redux利用

前言:

应用webpack曾经有些年头了,然而对于其中的一些根本配置还是只知其一;不知其二。为了成为一名优良的webpack配置工程师,也是学习了一把webpack,react的配置,特分享此次经验,并记录当中遇到的一些问题。当然当初的配置只是很根底的,心愿在当前的工作经验中,多多摸索,把一些webpack优化,react,redux最佳实际,都退出到其中。

文章目录

  • webpack根底配置
  • 配置react, less
  • 引入antd,
  • react-router的应用
  • react-redux
  • redux异步中间件的抉择 thunk/saga
  • 我的项目优化:MiniCssExtractPlugin,路由切割懒加载,postcss-loader, url-loader, hmr,tree shaking,
  • devserver proxy,本地mock数据
  • lint & prettier
  • 我的项目部署脚本

一. webpack根底配置

学习一个新技术,最好的获取形式便是浏览官网文档。(https://www.webpackjs.com/gui...)。通读当前,总结为以下几个要点。

  1. 初始化我的项目,装置依赖。
npm init -ynpm install webpack webpack-cli --save-dev
  1. 配置文件
// webpack.base.jsconst 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.jsfunction component() {    var element = document.createElement('div');    element.innerHTML = 'hello world hyt';    return element;}document.body.appendChild(component());
  1. 接下来运行 npm run dev,查看dist下输入,发现多了一个main.bundle.js文件,关上咱们新建的index.html文件,能够看到如下,阐明咱们的webpack根底打包曾经可能应用了。

  1. 如果咱们更改了一个入口终点的名称,或者针对多入口增加了一个新的名称,又须要咱们手动去index.html中去更改,咱们能够应用HtmlWebpackPlugin动静生成index.html.

当然,防止咱们每次手动去清空dist文件下的内容,能够应用clean-webpack-plugin插件帮忙清空。

npm install html-webpack-plugin clean-webpack-plugin// webpack.base.jsconst 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文件。

  1. 如上,咱们曾经把握了webpack打包编译的根本应用。

然而在日常开发中,每次批改完代码都须要手动执行webpack打包命令,很繁琐。这时候能够采纳 watch或者webpack-dev-server或者webpack-dev-middleware办法实现。较为罕用的是应用webpack-dev-server,不仅提供一个简略的 web 服务器,并且可能实时从新加载。

npm install --save-dev webpack-dev-serverconst 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.json

 "scripts": {    "dev": "webpack-dev-server --config webpackconfig/webpack.base.js",    "watch": "webpack --config webpackconfig/webpack.base.js --watch"  },

执行 npm run dev,看看成果。

  1. webpack-dev-server诚然好用,然而只实用于开发环境,在生产环境中,咱们的指标则转向于关注更小的 bundle,更轻量的 source map,以及更优化的资源,以改善加载工夫。所以咱们能够依据不同的环境,加载不同的webpack配置。

webpack.base.js是通用配置,webapck.dev.js中是开发环境配置,webapck.prod.js是生产环境配置。webpack-merge能够帮住咱们很好的合并配置。

接下来拆分配置:

// webpack.base.jsconst 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.jsconst { 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的解决。

二. 引入React, 解决css, less

  1. 装置React ,React-dom
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.jsnew HtmlWebpackPlugin({  title: 'hyt Management',  template: './src/template.html',}),

接下来运行,npm run dev,果然,报错了。

提醒咱们,应该须要专门的loader去解决咱们的js/jsx文件。这时候,就是赫赫有名的babel退场了。babel能够帮忙咱们进行js文件的编译转换。

  1. babel

除了帮忙咱们对于高版本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,咱们费了这么大的功夫,不得不吐槽)。

  1. 接下来给页面加点款式。

有了方才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.jsimport "./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.jsimport 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.jsimport 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,款式果然失效了

  1. less

既然都用到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的应用,以及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

接下来引入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去治理咱们的数据流。

五. Ract-redux

为什么抉择redux来治理咱们的数据流,以及redux的设计原理,能够查看阮一峰老师的系列文章,这里只给出根本应用。http://www.ruanyifeng.com/blo...

几个比拟重要的概念,Provider,connect, creatStore, reducer, applyMiddleware,actions。

持续革新文件构造及内容

npm install redux react-redux
  1. sotre
// src/store.jsimport { createStore } from "redux";import reducers from "./reducers/index";const store = createStore(reducers, {});export default store;
  1. reducer
// src/reducers/index.jsimport { 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,});
  1. provider
// src/index.jsimport { Provider } from "react-redux";import Routes from "./Routes";import store from "./store";const App = () => {  return (    <Provider store={store}>      <BrowserRouter>        <Routes />      </BrowserRouter>    </Provider>  );};
  1. connect

新建容器组件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);
  1. 同样在route中引入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的数据。

  1. dispatch actions

下面曾经获取到了store中的数据,接下来dispatch去扭转store中的数据,因为组件订阅了store(connect),页面数据源会主动渲染变更。

6.1 增加action types常量

// src/constants/actionTypes.jsexport const SET_USER_NAME = "SET_USER_NAME";

6.2 扭转store的action

// src/actions/homeAction.jsimport { SET_USER_NAME } from "../constants/actionsType";export function setName(payload) {  return { type: SET_USER_NAME, payload };}

6.3 承受actions的reducer

// src/reducers/index.jsimport { 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.jsimport 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.jsimport { 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.jsexport function getName(payload) {  return (dispatch) => {    return Promise.resolve().then((res) => {      dispatch({        type: SET_USER_NAME,        payload: {          name: "fetch mock",        },      });      return res;    });  };}
// src/containers/home.jsconst 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...

七. 我的项目优化

  1. 路由切割懒加载。应用import() + react-loadable实现。
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加载。

  1. 热更新HMR

因为当初咱们每改一下代码,都能够看到刷新一次页面,于是之前的路由跳转状态、表单中填入的数据都会重置。对于开发人员过程很不不便,这时候就引出咱们的热更新了,不会造成页面刷新,而是进行模块的替换。

// webpack.dev.jsmodule.exports = merge(base, {  mode: "development",  devtool: "inline-source-map",  devServer: {    contentBase: "./dist",    open: true,    port: 8888,    historyApiFallback: true,    hot: true, // +++++++  },});
// index.jsconst App = () => {  return (    <Provider store={store}>      <BrowserRouter>        <Routes />      </BrowserRouter>    </Provider>  );};++++if (module.hot) {  module.hot.accept();}++++ReactDOM.render(<App />, document.getElementById("root"));
  1. url-loader & file-loader

当初咱们的我的项目中还没有专门的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,        },      },    ],  },
  1. MiniCssExtractPlugin

该插件将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.jsconst 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({})],},
  1. tree shaking

https://webpack.docschina.org...

mode: 'production'