阅读 296

记录react-antd脚手架0-1搭建(逐渐完善)

场景

  • 新公司想搞一个自己用的脚手架,但是不想使用现成的,类似create-react-app这种,于是就决定自己搞一个,下面主要是记录自己粗略的搭建过程。
    项目获取地址

文件目录结构

需求

  1. react
  2. antd
  3. eslint
  4. less
  5. 热更新
  6. commit限制
  7. moment.js
  8. dayjs替换moment.js
  9. 数据mocker
  10. 请求代理 ...

开始

  • 首先安装node,npm环境,新建文件夹,并使用npm init初始化.
  • 安装webpack相关的包
npm i webpack webpack-cli -D
复制代码
  • 在项目目录下建一个config文件夹,用来存放webpack的配置文件
webpack.analyz.js // 存放打包分析的配置
webpack.common.js // 存放打包基础相同的配置
webpack.dev.js // 存放开发环境的配置
webpack.prod.js // 存放线上环境的配置
复制代码
  • 不同环境的配置通过webpack-merge来合并
npm i webpack-merge -D
复制代码

webpack.common.js


module.exports = {
    ...
    entry: ["./src/index.js"], // 入口
    output: { // 输出
        path: resolve("build"),
        filename: "[name].[hash].js",
        chunkFilename: "static/js/[name].[contenthash:8].chunk.js"
      },
}
复制代码

处理js、jsx、ts、tsx结尾的文件

npm i - D cache-loader
npm i - D babel-loader // es6转换
npm i - D @babel/preset-react 
npm i - D @babel/plugin-syntax-dynamic-import // 异步引入
npm i - D @babel/plugin-proposal-class-properties
npm i - D eslint-loader
 // 处理js、jsx、ts、tsx结尾的文件
 
module.exports = {
    ...
    module: {
        rule: {
            ...
            {
                test: /\.(js|jsx|ts|tsx)$/,
                exclude: /(node_modules|bower_components)/,
                use: [
                  "cache-loader", // 是否需要缓存
                  {
                    loader: "babel-loader",
                    options: {
                      presets: ["@babel/preset-react"],
                      plugins: [
                        "@babel/plugin-proposal-class-properties",
                        "@babel/plugin-syntax-dynamic-import"
                      ]
                    }
                  },
                  "eslint-loader"
                ]
            }
        }
    },
}
复制代码

处理less和css文件


npm i style-loader -D 
npm i css-loader -D 
npm i postcss-loader -D 
npm i autoprefixer -D // 添加不同浏览器样式头
npm i less-loader -D 

module.exports = {
    ...
    module: {
        rule: {
            ...
            {
            test: /\.(le|c)ss$/,
            use: [
              "cache-loader",
              "style-loader",
              {
                loader: "css-loader",
                options: {
                  importLoaders: 1
                }
              },
              {
                loader: "postcss-loader",
                options: {
                  plugins() {
                    return [
                      autoprefixer({
                        overrideBrowserslist: [">0.25%", "not dead"]
                      })
                    ]
                  }
                }
              },
              {
                loader: "less-loader",
                options: {
                  javascriptEnabled: true
                }
              }
            ]
        }
      }
    },
}
复制代码

处理图片、字体、音频等文件

npm i url-loader -D

module.exports = {
    ...
    module: {
        rule: {
            ...
            {
                test: /\.(png|jpe?g|svg|gif)(\?.*)?$/,
                loader: "url-loader",
                options: {
                  limit: 10240,
                  name: "static/img/[name].[hash:7].[ext]"
                }
              },
              {
                test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
                loader: "url-loader",
                options: {
                  limit: 10240,
                  name: "static/font/[name].[hash:7].[ext]"
                }
              },
              {
                test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
                loader: "url-loader",
                options: {
                  limit: 10240,
                  name: "static/media/[name].[hash:7].[ext]"
                }
              }
        }
    }
}
复制代码

创建.bablerc文件处理语法转换,添加热更新及antd组件按需加载

npm i react-hot-loader // 热更新
npm i babel-plugin-import -D // antd组件按需加载
{
    "presets": ["@babel/preset-env"],
    "plugins": [
        "react-hot-loader/babel", // 热更新配置项
        ["@babel/plugin-transform-runtime",
            {
                "corejs": 3
            }
        ],
        "@babel/plugin-syntax-dynamic-import",
        ["import", { // antd组件按需加载
            "libraryName": "antd", 
            "style": "css"  // `style: true` 会加载 less 文件
        }]
    ]
}
复制代码

热更新修改入口文件src/index.js

import React from "react"
import ReactDOM from "react-dom"
import { hot } from "react-hot-loader/root"
import Parent from "./components/Parent"

const App = () => (
  <div>
    <Parent />
  </div>
)
hot(App)
const render = Component => {
  ReactDOM.render(<Component />, document.getElementById("root"))
}

render(App)
复制代码

设置html打包模版,安装html-webpack-plugin

npm i -D html-webpack-plugin

module.exports = {
    ...
    plugins: [
        ...
        new HtmlWebpackPlugin({
          template: "./public/index.html", // 模版文件路径
          inject: "body"
        }),
    ]
}
复制代码

设置全局变量


  plugins: [
    ...
    new webpack.ProvidePlugin({
      // 全局变量
      React: "react",
      Component: ["react", "Component"],
      PureComponent: ["react", "PureComponent"]
    })
  ],
复制代码

设置文件类型的查找顺序和别名

  resolve: {
    extensions: [".js", ".jsx", ".ts", ".tsx"],
    alias: {
      "react-dom": "@hot-loader/react-dom"
    }
  }
复制代码

设置eslint,规则采用的是prettier 加上airbnb。

// 安装
npm i -D eslint
npm i -D eslint-plugin-import
npm i -D eslint-config-airbnb
npm i -D eslint-config-prettier
npm i -D eslint-plugin-prettier
npm i -D eslint-plugin-jsx-a11y
npm i -D eslint-plugin-react-hooks
// .eslintrc.js
module.exports = {
    "root": true,
    "env": {
        "node": true,
        "browser": true,
        "es6": true
    },
    "extends": [
        "airbnb-base",
        "prettier",
        "plugin:prettier/recommended",
        "plugin:react/recommended",
        "eslint:recommended",
    ],
    "globals": {
        "Atomics": "readonly",
        "SharedArrayBuffer": "readonly"
    },
    "parserOptions": {
        "parser": 'babel-eslint',
        "ecmaFeatures": {
            "jsx": true
        },
        "ecmaVersion": 2018,
        "sourceType": "module"
    },
    "plugins": [
        "react",
        "prettier",
    ],
    "rules": {
        "no-console": process.env.NODE_ENV === 'production' ? 2 : 0
    },
    "settings": {
        "react": {
            "version": "detect"
        }
    }
};
复制代码

配置生产环境压缩去掉consloe.log

// webpack.prod.js
module.exports = {
    plugins:[
    ....
    new Uglify({
          uglifyOptions: {
            compress: {
              warnings: false,
              drop_console: true, // console
              pure_funcs: ['console.log'] // 移除console
            }
          },
          parallel: true
        }),
    ]
}
复制代码

webpack.dev.js

  • 使用webpack-merge合并webpack.common.js,然后设置dev开发环境的模式(development)以及环境变量(dev),然后安装启动本地服务的webpack-dev-server
npm i -D webpack-dev-server
// webpack.dev.js
const webpack = require("webpack")
const merge = require("webpack-merge")
const common = require("./webpack.common")

module.exports = merge(common, {
  mode: "development",
  devServer: {
    contentBase: "./index.html",
    hot: true,
    port: 8080, // 端口
    open: true, // 是否打开浏览器
    inline: true,
    historyApiFallback: true
  },
  plugins: [
    new webpack.DefinePlugin({
      DEV: JSON.stringify("dev"), // 字符串
      FLAG: "true" // FLAG 是个布尔类型
    }),
    new webpack.HotModuleReplacementPlugin() // 热更新插件
  ]
})
}
// index.js
if(ENV === 'dev') {
    ...
}
复制代码
  • 如果将打包体积时间分析放在webpack.common.js中,正常启动热更新之后,打包编译速度变慢,修改页面后响应速度也会变慢(每次改变都会重新生成stats.json文件),所以将打包分析单独抽出来做成一个命令,后面的webpack.analyz.js就是由此产生的。

webpack.prod.js

  • 使用webpack-merge合并webpack.common.js,然后设置prod开发环境的模式(mode: production)以及环境变量(production),然后开启css压缩以及代码分割,每次打包将上一次的打包文件清除。
npm i mini-css-extract-plugin
npm i clean-webpack-plugin
npm i optimize-css-assets-webpack-plugin
npm i terser-webpack-plugin
// webpack.prod.js
const webpack = require("webpack")
const merge = require("webpack-merge")
const MiniCssExtractPlugin = require("mini-css-extract-plugin") // css拆分
const { CleanWebpackPlugin } = require("clean-webpack-plugin") // 每次打包清除上一次
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin") // css压缩
const TerserJSPlugin = require("terser-webpack-plugin")
const common = require("./webpack.common")

module.exports = merge(common, {
  mode: "production",
  devtool: "cheap-module-source-map",
  optimization: {
    splitChunks: {
      chunks: "all"
    },
    minimizer: [new TerserJSPlugin({}), new OptimizeCSSAssetsPlugin({})]
  },
  plugins: [
    new webpack.DefinePlugin({
      DEV: JSON.stringify("production"), // 字符串
      FLAG: "true" // FLAG 是个布尔类型
    }),
    new CleanWebpackPlugin(),
    new MiniCssExtractPlugin({
      filename: "static/css/[name].css", // 打包到static的css目录下
      ignoreOrder: false // Enable to remove warnings about conflicting order
    })
  ]
})

复制代码

build打包后文件包含项目本地的绝对路径

  • 在使用npm run build后,build下的main.[hash:5].js文件中出现项目的绝对路径,产生原因是因为开发环境使用了react-hot-loader,在.babelrc文件中有相关的配置,处理方法是安装cross-env,在package.json中增加环境变量,是用babel.config.js代替.babelrc使用process.env.NODE_ENV区分环境,根据环境是否使用react-hot-loader。如果你看了react-hot-loader npm上的文档安装了@hot-loader/react-dom,并在webpack中配置了别名为react-dom,建议一并删除。

npm i -D cross-env
// babel.config.js
const plugins = [
  [
    '@babel/plugin-transform-runtime',
    {
      corejs: 3
    }
  ],
  '@babel/plugin-syntax-dynamic-import',
  [
    'import',
    {
      libraryName: 'antd',
      style: 'css' // `style: true` 会加载 less 文件
    }
  ]
];
if (process.env.NODE_ENV === 'development') {
  plugins.unshift('react-hot-loader/babel');
}
module.exports = {
  presets: ['@babel/preset-env'],
  plugins
};

// package.json
{
  ...
  "scripts": {
    "dev": "cross-env NODE_ENV=development...",
    "build": "cross-env NODE_ENV=production ...",
    }
}

// webpack.common.js
module.exports = {
    ...,
  resolve: {
    alias: {
      'react-dom': '@hot-loader/react-dom', // 建议将此删除
    }
}

复制代码
  • 重新执行npm run build后文件中就没有项目本地的绝对路径了。

添加压缩及文件切割

  • 配置optimize-css-assets-webpack-plugin做css的代码压缩,uglifyjs-webpack-plugin做js代码的压缩;用optimization做文件的切割,webpack SplitChunksPlugin 官网地址。
npm i optimize-css-assets-webpack-plugin // css压缩
npm i uglifyjs-webpack-plugin // js压缩

const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin'); 
const UglifyjsWebpackPlugin = require('uglifyjs-webpack-plugin');
// webpack.prod.js
module.exports = {
  optimization: {
    splitChunks: {
      cacheGroups: {
        vendors: {
          test: /[\\/]node_modules[\\/]/, // 包括的目录
          name: 'vendors',
          minSize: 50000, // 块的最小大小
          minChunks: 1, // 在拆分之前共享模块的最小块数
          chunks: 'initial', // 共有3个值"initial""async""all"。配置后,优化仅选择初始块,按需块或所有块
          priority: 1 // 该配置项是设置处理的优先级,数值越大越优先处理
        },
        commons: {
          test: /[\\/]src[\\/]/,
          name: 'commons',
          minSize: 50000,
          minChunks: 2,
          chunks: 'initial',
          priority: -1,
          reuseExistingChunk: true // 这个配置允许我们使用已经存在的代码块
        }
      }
    },
  },
  plugins: [
    new OptimizeCSSAssetsPlugin({
        assetNameRegExp: /\.(le|c)ss$/g,
        cssProcessorOptions: {
          safe: true,
          autoprefixer: { disable: true }, // 禁用掉cssnano对于浏览器前缀的处理
          mergeLonghand: false,
          discardComments: {
            removeAll: true // 移除注释
          }
        },
        canPrint: true
      }),
      
    new UglifyjsWebpackPlugin({
      uglifyOptions: {
        exclude: /\.min\.js$/, // 过滤掉以".min.js"结尾的文件,我们认为这个后缀本身就是已经压缩好的代码,没必要进行二次压缩
        extractComments: false, // 移除注释
        cache: true,
        compress: {
          warnings: false,
          drop_console: true, // console
          pure_funcs: ['console.log'] // 移除console
        }
      },
      parallel: true // 开启并行压缩,充分利用cpu
    }),
    ]
}
复制代码

性能优化

module.exports = {
  performance: {
    hints: 'warning',
    maxAssetSize: 500000, // 单个资源体积最大,超过会提示
    maxEntrypointSize: 500000,
    assetFilter(assetFilename) {
      // 只计算js文件的性能提示
      return assetFilename.endsWith('.js');
    }
  },
复制代码

webpack.analyz.js

  • 使用webpack-merge合并webpack.common.js,新建一个输出文件为analyz,添加分析插件webpack-bundle-analyzer,并在package.json,中添加两个命令。
npm i webpack-bundle-analyzer
// webpack.analyz.js
const path = require("path")
const merge = require("webpack-merge")
const { BundleAnalyzerPlugin } = require("webpack-bundle-analyzer") // 分析我们打包的代码
const prod = require("./webpack.prod")

function resolve(dir) {
  return path.join(__dirname, '..', dir);
}
module.exports = merge(prod, {
  output: {
    path: resolve('analyz')
  },
  plugins: [
    new BundleAnalyzerPlugin({
      analyzerMode: "disabled", // 避免每次启动都打开分析网站
      generateStatsFile: true
    })
  ]
})

// package.json
scripts:{
    ...
    "analyz": "webpack --config ./config/webpack.analyz.js",
    "analyz-web": "webpack-bundle-analyzer --port 8001 ./analyz/stats.json",
}
复制代码

引入moment

  • 正常直接引入moment会将moment所有的语言包全部打包进来,所有需要使用moment-locales-webpack-plugin插件,只需要引入我们需要的语言包即可。
npm i moment
npm i moment-locales-webpack-plugin
// webpack.common.js
const MomentLocalesPlugin = require("moment-locales-webpack-plugin")
module.exports = {
    ...
    plugins: [
        MomentLocalesPlugin(), // 剥离除 “en” 以外的所有语言环境。
        // 或者:剥离除 “en”、“es-us” 和 “ru” 以外的所有语言环境。
        //(“en” 内置于 Moment 中,无法移除)
        new MomentLocalesPlugin({
            localesToKeep: ['es-us', 'ru'],
        }),
    ]
}
复制代码

dayjs替换moment

  • dayjs是antd推荐的,优点是体积更小,moment生态更好,API上并无差异,所以具体怎么选择还是看团队。
npm i dayjs
npm i antd-dayjs-webpack-plugin 
// webpack.common.js
const AntdDayjsWebpackPlugin = require("antd-dayjs-webpack-plugin")
module.exports = {
    ...
    plugins: [
        new AntdDayjsWebpackPlugin(),
    ]
}
复制代码
  • dayjs打包后大小
  • moment打包后大小

  • 使用dayjs后,直接npm run analyz,可能会报错,内容如下
  • 如果是这个错的话,是由于UglifyJs只支持ES5dayjs可能引入了一部分ES6的写法,所以导致webpack打包失败,用beta版本的Uglify-es来代替UglifyJs
npm i -D uglifyjs-webpack-plugin@beta
复制代码

git commit 限制

参考husky+ prettier + commitlint 提交前代码检查和提交信息规范

  • 如果设置后,校验未生效,可以将node_modules删除重新安装再尝试。

git add [filepath] -u不生效

  • 如果你是用了commit的添加检测eslint,那你可能出现上述命令不生效的情况,原因是在执行commit的时候会主动检测是否提交内容符合规范,然后主动修复,主动修复后会执行git add .命令,导致所有变化文件全部提交,解决方法是修改package.json文件中lint-staged对象下的git add .git add即可。

数据mocker

  • 在正常前后端分离的开发过程中,后端还没有给出可用接口,那我们需要做测试就需要自己构造数据,按装mocker-api,然后配置开发环境的webpack的配置文件webpack.dev.jsDevServer,添加以下配置:
npm i -D mocker-api
// webpack.dev.js
const path = require('path');
const apiMocker = require('mocker-api');

module.exports = {
  devServer: {
  ...
  before(app) {
    // mocker数据的文件地址
    apiMocker(app, path.resolve('./mock/mocker.js'));
  },
}
// mock/mocker.js
module.exports = {
  'GET /user': {
    data: { name: 'detanx' },
    status: 200,
    msg: 'success!'
  },
}

// index.js
axios.get('/user').then(res => {
    console.log(res)
}).catch(err => {
    console.log(err)
})
复制代码

请求代理

  • 当后端接口已经准备好了,我们需要请求后端的接口,那就需要用到webapck-dev-server中的请求代理,如果之前有使用mock,我们需要将mock部分的配置注释掉,然后再添加代理的配置
// webpack.dev.js
module.exports = {
  devServer: {
    ...,
    // before(app) {
    //   apiMocker(app, path.resolve('./mock/mocker.js'));
    // },
    proxy: {
      '/stockserver': {
        target: 'http://192.168.10.60:8080', // 代理地址
        changeOrigin: true, // 是否允许域名地址
        secure: false
      }
    },
  }
}
复制代码

搭建参考

detanx blog
webpack 官网
使用webpack4从零配置react项目
4W字长文带你深度解锁Webpack系列(上)
万字长文带你深度解锁Webpack(进阶篇)

  • 对你有帮助的话,给个赞👍呗,感谢支持!