webpack 漸進式配置 —— React 通用版

版本說明

此版本適用於任意 react 項目,並無特殊依賴。此版本主要用於爲其他定製版本做鋪墊,省去一些通用的配置解釋。同時能夠實現漸進式的應用,有利於同學們的學習。

版本功能

react、react-router、lodash、moment、less、css-modules、eslint、eslint-config-airbnb

安裝命令

yarn add react react-dom react-router-dom lodash moment

yarn add -D cross-env \
	webpack webpack-cli webpack-merge webpack-dev-server \
	file-loader url-loader \
	style-loader css-loader less less-loader postcss-loader autoprefixer \
	babel-loader @babel/core @babel/preset-env @babel/runtime @babel/preset-react \
	@babel/plugin-transform-runtime babel-plugin-lodash \
	html-webpack-plugin mini-css-extract-plugin optimize-css-assets-webpack-plugin uglifyjs-webpack-plugin eslint-import-resolver-webpack

npx install-peerdeps -D eslint-config-airbnb

特別說明

  • cross-env 可以保證 windows 系統下的兼容
  • babel-plugin-lodash 的安裝,優化了 lodash 的打包體積
  • IgnorePlugin 優化了 moment 的打包,去掉了所有的 locale,需要手動加載,參考這裏
  • eslint 的安裝比較特別,可參考這裏
  • IDE 需要安裝 eslint 的擴展才能提示
  • eslint-import-resolver-webpack 可以解決 webpack.resolve.alias 的誤報問題
  • postcss、eslint 的配置因爲較簡單,都寫到了 package.json 中,並沒有新建 .eslintrc.jspostcss.config.js,詳見下文

項目目錄

webpack-demo
├── config
│   ├── webpack.base.js
│   ├── webpack.dev.js
│   └── webpack.prod.js
├── src
│   ├── apps
│   │   ├── Home
│   │   │   ├── index.jsx
│   │   │   └── style.less
│   │   ├── Lodash.jsx
│   │   └── Moment.jsx
│   └── index.jsx
├── .gitignore
├── index.html
├── package.json
├── webpack.config.js
└── yarn.lock

配置文件詳情

package.json

{
  "name": "webpack-demo",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "scripts": {
    "dev": "cross-env NODE_ENV=development webpack-dev-server",
    "build": "cross-env NODE_ENV=production webpack --progress --colors"
  },
  "dependencies": {
    "lodash": "^4.17.11",
    "moment": "^2.24.0",
    "react": "^16.8.6",
    "react-dom": "^16.8.6",
    "react-router-dom": "^5.0.0"
  },
  "devDependencies": {
    "@babel/core": "^7.4.4",
    "@babel/plugin-transform-runtime": "^7.4.4",
    "@babel/preset-env": "^7.4.4",
    "@babel/preset-react": "^7.0.0",
    "@babel/runtime": "^7.4.4",
    "autoprefixer": "^9.5.1",
    "babel-loader": "^8.0.5",
    "babel-plugin-lodash": "^3.3.4",
    "cross-env": "^5.2.0",
    "css-loader": "^2.1.1",
    "eslint": "5.3.0",
    "eslint-config-airbnb": "17.1.0",
    "eslint-import-resolver-webpack": "^0.11.1",
    "eslint-plugin-import": "^2.14.0",
    "eslint-plugin-jsx-a11y": "^6.1.1",
    "eslint-plugin-react": "^7.11.0",
    "file-loader": "^3.0.1",
    "html-webpack-plugin": "^3.2.0",
    "less": "^3.9.0",
    "less-loader": "^5.0.0",
    "mini-css-extract-plugin": "^0.6.0",
    "optimize-css-assets-webpack-plugin": "^5.0.1",
    "postcss-loader": "^3.0.0",
    "style-loader": "^0.23.1",
    "uglifyjs-webpack-plugin": "^2.1.2",
    "url-loader": "^1.1.2",
    "webpack": "^4.30.0",
    "webpack-cli": "^3.3.2",
    "webpack-dev-server": "^3.3.1",
    "webpack-merge": "^4.2.1"
  },
  "postcss": {
    "plugins": {
      "autoprefixer": {}
    }
  },
  "eslintConfig": {
    "root": true,
    "extends": [
      "airbnb"
    ],
    "env": {
      "browser": true,
      "node": true,
      "es6": true,
      "jest": true
    },
    "settings": {
      "import/resolver": {
        "webpack": {
          "config": "./webpack.config.js"
        }
      }
    }
  }
}

webpack.config.js

let config;
switch (process.env.NODE_ENV) {
  case "development":
    config = require("./config/webpack.dev.js");
    break;
  case "production":
  default:
    config = require("./config/webpack.prod.js");
}

module.exports = config;

config/webpack.base.js

const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const webpack = require('webpack');

const NODE_ENV = process.env.NODE_ENV || "development";
const PROJECT_PATH = path.resolve(__dirname, `../`);

module.exports = {
  mode: NODE_ENV,
  entry: {
    index: path.resolve(PROJECT_PATH, "./src/index")
  },
  output: {
    path: path.resolve(PROJECT_PATH, "./build"),
    filename: `js/[name]${
      NODE_ENV === "production" ? "_[contenthash:6]" : ""
    }.js`,
    libraryTarget: "umd"
  },
  module: {
    rules: [
      {
        test: /\.m?jsx?$/,
        include: [path.resolve(PROJECT_PATH, "src/")],
        use: {
          loader: "babel-loader",
          options: {
            presets: ["@babel/preset-env", "@babel/preset-react"],
            plugins: ["@babel/plugin-transform-runtime", "lodash"]
          }
        }
      },
      /* copy from vue-cli start */
      /* config.module.rule('images') */
      {
        test: /\.(png|jpe?g|gif|webp)(\?.*)?$/,
        use: [
          /* config.module.rule('images').use('url-loader') */
          {
            loader: "url-loader",
            options: {
              limit: 4096,
              fallback: {
                loader: "file-loader",
                options: {
                  name: "img/[name]_[hash:8].[ext]"
                }
              }
            }
          }
        ]
      },
      /* config.module.rule('svg') */
      {
        test: /\.(svg)(\?.*)?$/,
        use: [
          /* config.module.rule('svg').use('file-loader') */
          {
            loader: "file-loader",
            options: {
              name: "img/[name]_[hash:8].[ext]"
            }
          }
        ]
      },
      /* config.module.rule('media') */
      {
        test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
        use: [
          /* config.module.rule('media').use('url-loader') */
          {
            loader: "url-loader",
            options: {
              limit: 4096,
              fallback: {
                loader: "file-loader",
                options: {
                  name: "media/[name]_[hash:8].[ext]"
                }
              }
            }
          }
        ]
      },
      /* config.module.rule('fonts') */
      {
        test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/i,
        use: [
          /* config.module.rule('fonts').use('url-loader') */
          {
            loader: "url-loader",
            options: {
              limit: 4096,
              fallback: {
                loader: "file-loader",
                options: {
                  name: "fonts/[name]_[hash:8].[ext]"
                }
              }
            }
          }
        ]
      }
      /* copy from vue-cli end */
    ]
  },
  resolve: {
    extensions: [".mjs", ".js", ".jsx", ".json", ".ts", ".tsx"],
    alias: {
      "@": path.resolve(PROJECT_PATH, "./src/")
    }
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: path.resolve(PROJECT_PATH, "./index.html")
    }),
    new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/)
  ],
  optimization: {
    chunkIds: "named",
    moduleIds: "hashed",
    splitChunks: {
      chunks: "initial",
      name: "vendors"
    },
    runtimeChunk: {
      name: entrypoint => `runtime_${entrypoint.name}`
    }
  }
};

config/webpack.dev.js

const webpackMerge = require("webpack-merge");
const baseConfig = require("./webpack.base");

const config = {
  module: {
    rules: [
      {
        test: /\.(less|css)$/,
        use: [
          "style-loader",
          {
            loader: "css-loader",
            options: {
              importLoaders: 2,
              modules: true,
              localIdentName: "[folder]-[local]-[hash:base64:5]"
            }
          },
          "postcss-loader",
          "less-loader"
        ]
      }
    ]
  },
  devtool: "cheap-module-eval-source-map"
};

module.exports = webpackMerge(config, baseConfig);

config/webpack.prod.js

const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const webpackMerge = require("webpack-merge");
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");
const UglifyJsPlugin = require("uglifyjs-webpack-plugin");
const baseConfig = require("./webpack.base");

const config = {
  module: {
    rules: [
      {
        test: /\.(less|css)$/,
        use: [
          MiniCssExtractPlugin.loader,
          {
            loader: "css-loader",
            options: {
              importLoaders: 2,
              modules: true,
              localIdentName: "[folder]-[local]-[hash:base64:5]"
            }
          },
          "postcss-loader",
          "less-loader"
        ]
      }
    ]
  },
  plugins: [
    new MiniCssExtractPlugin({
      filename: "css/[name]_[contenthash:6].css",
      allChunks: true
    })
  ],
  optimization: {
    minimizer: [
      new UglifyJsPlugin({
        cache: true,
        parallel: true,
        sourceMap: false // set to true if you want JS source maps
      }),
      new OptimizeCSSAssetsPlugin({})
    ]
  }
};

module.exports = webpackMerge(config, baseConfig);

.gitignore

# copy from vue-cli and create-react-app

# dependencies
/node_modules
/.pnp
.pnp.js

# testing
/coverage
/tests/e2e/reports/
selenium-debug.log

# production
/build
/dist

# misc
.DS_Store
.env.local
.env.*.local

# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

Demo 文件詳情 TL;DR

index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>Demo</title>
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>

src/index.jsx

import React from "react";
import ReactDOM from "react-dom";
import { BrowserRouter as Router, Route, Link } from "react-router-dom";
import Home from "@/apps/Home";
import Lodash from "@/apps/Lodash";
import Moment from "@/apps/Moment";

ReactDOM.render(
  <Router>
    <nav>
      <ul>
        <li>
          <Link to="/">Home</Link>
        </li>
        <li>
          <Link to="/lodash/">Lodash</Link>
        </li>
        <li>
          <Link to="/moment/">Moment</Link>
        </li>
      </ul>
    </nav>
    <Route path="/" exact component={Home} />
    <Route path="/lodash/" component={Lodash} />
    <Route path="/moment/" component={Moment} />
  </Router>,
  document.getElementById("root")
);

src/apps/Lodash.jsx

import React from "react";
import _ from "lodash";

const Lodash = props => (
  <div>
    <h1>Lodash</h1>
    <p>{`The result of \`_.camelCase('camel-case')\` is ${_.camelCase(
      "camel-case"
    )}`}</p>
  </div>
);
export default Lodash;
src/apps/Moment.jsx
import React from "react";
import moment from "moment";

const Moment = props => (
  <div>
    <h1>Moment</h1>
    <p>{moment().format()}</p>
  </div>
);
export default Moment;

src/apps/Home/index.jsx

import React from "react";
import style from "./style.less";

const Home = props => (
  <div className={style.home}>
    <h1>Home</h1>
    <p>This is Home page which uses CSS-Modules</p>
  </div>
);
export default Home;

src/apps/Home/style.less

.home {
  text-align: center;
  color: red;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章