场景
- 新公司想搞一个自己用的脚手架,但是不想使用现成的,类似
create-react-app这种,于是就决定自己搞一个,下面主要是记录自己粗略的搭建过程。
项目获取地址
文件目录结构
需求
- react
- antd
- eslint
- less
- 热更新
- commit限制
- moment.js
- dayjs替换moment.js
- 数据mocker
- 请求代理 ...
开始
- 首先安装
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
}),
]
}
性能优化
- 通过配置
performance来增加或消除一些不必要的性能提示,webpack performance 。
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只支持ES5而dayjs可能引入了一部分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.js的DevServer,添加以下配置:
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(进阶篇)
- 对你有帮助的话,给个赞👍呗,感谢支持!