react-webpack-base(github)
从零开始搭建 React 基础框架
一、技术选型
webpack 5 + react17 + typescript + react-router-dom v6
- 规范代码风格
- 常用命令
# 开发
npm run start
# 生产打包
npm run build
# 包体积大小分析
npm run analyzer
二、webpack 配置、loaders 和 plugins
1. 基本配置
- 安装
# webpack:用于编译 JavaScript 模块; webpack-cli:用于在命令行中运行 webpack
npm install webpack webpack-cli -D
- webpack 配置拆分
webpack.common.js # 公共
webpack.dev.js # 开发
webpack.prod.js # 生产
webpack.analyzer.js # 包体积大小分析
- webpack-dev-server:在本地起一个 http 服务
# 安装
npm install webpack-dev-server -D
# package.json 配置 scripts
"serve": "webpack serve"
- 反向代理:解决本地开发跨域问题
// webpack.config.js
module.exports = {
...,
devServer: {
port: 9000,
historyApiFallback: true, // history 路由
proxy: {
'/baseapis': {
target: 'http://test-groupbuy-api.chenxuan100.cn',
// 是否启用websocket
ws: false,
//是否允许跨域
changeOrigin: true,
pathRewrite: {
'^/baseapis': ''
}
}
}
}
};
2. loader
- less-loader : 处理 css 文件打包
npm install css-loader style-loader less less-loader -D
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.less$/i,
loader: [
// compiles Less to CSS
'style-loader',
'css-loader',
'less-loader'
]
}
]
}
};
3. plugins
- html-webpack-plugin: 用于打包 html 文件
该插件将为你生成一个 HTML5 文件, 在 body 中使用 script 标签引入你所有 webpack 生成的 bundle。
npm install html-webpack-plugin
// webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
...,
plugins: [
new HtmlWebpackPlugin({
filename: 'public/index.html'
})
]
};
- simple-progress-webpack-plugin: 观察打包进度
npm install simple-progress-webpack-plugin -D
// craco.config.js
webpack: {
plugins: [
// 查看打包的进度
new SimpleProgressWebpackPlugin()
];
}
- webpack-merge:用于合并 webpack 公共配置
const { merge } = require('webpack-merge');
const common = require('./webpack.common.js');
module.exports = merge(common, {
mode: 'production'
});
- webpack-bundle-analyzer:包体积大小分析
// webpack.analyzer.js 包体积大小分析
const { merge } = require('webpack-merge');
const common = require('./webpack.common.js');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = merge(common, {
mode: 'production',
plugins: [new BundleAnalyzerPlugin()]
});
-
- webpack split chunks : 分包
从 webpack v4 开始,移除了 CommonsChunkPlugin,取而代之的是 optimization.splitChunks
// 将react、react-dom、合并一个包(vendor.js)
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
minSize: 20000,
minRemainingSize: 0,
minChunks: 1,
maxAsyncRequests: 30,
maxInitialRequests: 30,
enforceSizeThreshold: 50000,
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
name: 'vendor',
chunks: 'all'
}
}
}
}
};
三、TypeScript 支持
- 安装依赖
npm install --save-dev typescript ts-loader
module.exports = {
...,
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
],
},
resolve: {
extensions: ['.tsx', '.ts', '.js'],
},
...
};
- 遇到问题
// 以下写法在ts上报错
import React from 'react';
// 解决方法一:需调整为
import * as React from 'react';
// 解决方法二:不需调整,增加tsconfig.json 配置
"allowSyntheticDefaultImports": true,
// ts报错类型“NodeRequire”上不存在属性“context”
// 解决方法
pnpm install @types/webpack-env -D
// 别名配置
// webpack.config.js
const path = require('path');
const resolve = (dir) => path.resolve(__dirname, dir);
module.exports = {
resolve: {
alias: {
'@': resolve('src')
}
}
}
// tsconfig.json
{
"compilerOptions": {
"baseUrl": "./",
"paths": {
"@/*": ["src/*"]
}
},
}
// 引入React无智能提示,报错“React”指 UMD 全局,但当前文件是模块。请考虑改为添加导入。ts(2686)的问题
// 修改tsconfig.json配置
{
"compilerOptions": {
"jsx": "react-jsx",
}
}
四、react 支持
1. react 和 react-dom
npm install react react-dom
2. 约定式路由 + 路由懒加载
- 目录
src/router;
- 依赖安装
# react-router-dom v6
npm install react-router-dom
# types
npm i -D @types/react-router-dom
约定式路由(去中心化)
- 1.最简单的情况
// 以下目录结构
└── pages
├── index.n.tsx
├── login
| └── index.n.tsx
└── 404.n.tsx
// 会得到以下配置的路由
[
{path: '/', element: '@/pages/index'},
{path: '/login', element: '@/pages/login/index'},
{path: '/404', element: '@/pages/404'},
]
- 2.动态路由
约定 `[]` 包裹的文件或文件夹为动态路由。
src/pages/invoice/[id].tsx 会成为 /invoice/:id
// 以后再考虑
src/pages/invoice/[id]/settings.tsx 会成为 /invoice/:id/settings
路由懒加载router lazy loading
踩坑1:about/index.tsx 刚开始被 require.context('@/pages', true, /\.tsx$/) 缓存过,一直调试懒加载都失败; 后面把约定式的路由 改成识别 .n.tsx 结尾的; 懒加载的正常书写 .tsx
五、抽离 API 层,二次封装 axios
- 第一步:确定想要的效果
api.user.loginIn(params).then(() => {});
- 目录结构, 具体实现可看源码; 为了防止接口被更改,使用了 Object.freeze
├── src # 源码目录
│ │── api # 接口目录
│ │ │── index.ts # 暴露出去的访问入口
│ │ │── shop.ts # shop 模块的接口
│ │ └── user.ts # user 模块的接口
│ └── services # services 层
│ │── index.ts # 请求库(axios) 的二次封装
│ └── config.ts # config 配置
- 优化前: src/api/index.ts
import services from '@/services';
import user from './user';
import shop from './shop';
const modelsFile = require.context('@/api', true, /.ts$/);
const models = modelsFile.keys().map((v) => {
console.log('v:', v);
return modelsFile(v);
});
console.log(models);
const api: any = {};
// 注册模块方法
function register(name: string, module: any) {
api[name] = {};
for (const key in module) {
let options = module[key];
let method = options.method;
api[name][key] = (params: any) => {
if (method === 'get') {
options.params = params;
} else {
options.data = params;
}
return services.request({ ...options });
};
}
// 冻结对象,不允许修改
Object.freeze(api[name]);
}
// 注册 或者使用require.context 自动导入注册
register('user', user);
register('shop', shop);
Object.freeze(api);
export default api;
- 优化: 使用 require.context 批量导入接口
npm i @types/webpack-env @types/node -D
// src/api/index.ts
import services from '@/services';
const moduleFiles = require.context('@/api', true, /.ts$/);
let moduleKeys = moduleFiles.keys().filter((v) => v !== './index.ts');
const api: any = {};
// 注册模块方法
function register(name: string, module: any) {
api[name] = {};
for (const key in module) {
let options = module[key];
let method = options.method;
api[name][key] = (params: any) => {
if (method === 'get') {
options.params = params;
} else {
options.data = params;
}
return services.request({ ...options });
};
}
// 冻结对象,不允许修改
Object.freeze(api[name]);
}
// 自动注册
moduleKeys.forEach((v) => {
console.log(moduleFiles(v).default);
let keys = v.split('.')[1].slice(1);
if (keys) register(`${keys}`, moduleFiles(v).default);
});
Object.freeze(api);
export default api;