前言
这篇文章主要是介绍我在公司对于 react 的开发历史,也算记录自己的学习经历。
起航
公司 19 年才开始使用 react 框架,刚用的时候,是公司领导找的一个朋友来从头开始教我们搭建脚手架,初期版本用的是 react 15+webpack 3.x 版本搭建的,这个架子基本上算是领导朋友搭建的,我们只是在这上面做开发。在这个版本时期,我个人对于 react 还停留在会用阶段,脚手架搭建也不会,只能在前人的基础上开发,早期公司用 react 主要是开发移动端项目。
var webpack = require('webpack')
var path = require('path')
const ExtractTextPlugin = require('extract-text-webpack-plugin')
module.exports = {
externals: {
BMap: 'BMap',
},
entry: {
vendor: ['react', 'react-dom', 'jquery'],
'babel-polyfill': 'babel-polyfill',
app: path.resolve(__dirname, '../src/app.js'),
},
output: {
path: path.resolve(__dirname, `./dist`),
publicPath: '/',
filename: '[name]-[hash].js',
},
resolve: {
alias: {
common: path.resolve(__dirname, '../src/common'),
component: path.resolve(__dirname, '../src/common/component'),
static: path.resolve(__dirname, '../src/static'),
},
extensions: ['.web.js', '.js', '.json', '.jsx', '.less', '.css', '.scss'],
},
module: {
rules: [
{
test: /\.(js|jsx)$/,
use: ['babel-loader'],
},
{
test: /\.(less|css)$/,
use: ExtractTextPlugin.extract({
fallback: 'style-loader',
use: [
{
loader: 'css-loader',
options: {
minimize: true,
sourceMap: true,
},
},
{
loader: 'less-loader',
options: {
sourceMap: true,
},
},
],
}),
},
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
use: [
{
loader: 'url-loader',
options: {
limit: 8192,
name: 'img/[name].[hash:7].[ext]',
//prefix: 'img'
},
},
],
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
use: [
{
loader: 'url-loader',
options: {
limit: 8192,
name: 'fonts/[name].[hash:7].[ext]',
},
},
],
},
],
},
devServer: {
proxy: {
'/portal': {
target: 'https://www.api.com/',
changeOrigin: true,
pathRewrite: {
'^/portal': '/portal',
},
},
},
host: 'localhost',
},
plugins: [
new webpack.optimize.CommonsChunkPlugin({
filename: 'vendor.js',
name: ['jquery'],
}),
new webpack.ProvidePlugin({
$: 'jquery',
jQuery: 'jquery',
Promise: 'es6-promise',
}),
new webpack.ProvidePlugin({
'window.store': 'store2',
store: 'store2',
}),
new ExtractTextPlugin({
filename: 'index-[contenthash].css',
disable: false,
allChunks: true,
}),
],
}
深入
20年的时候,我的组长准备离职,我就接任了他的任务,继续维护公司的前端项目。
在开发了一段时间后,觉得早期直接用 webpack 搭建的脚手架不太友好,很多东西都要自己去下载,去配置。后面了解到 react 官方推荐的 create-react-app(cra),就着手升级脚手架。用 cra 搭建了两个版本的架子,早期使用 react-app-rewired+customize-cra 来组合配置管理 webpack,后面觉得太麻烦又换成了 craco 来管理 webpack,react 版本也升级到了 16.8 以后的版本。
const path = require('path')
const webpack = require('webpack')
const CracoLessPlugin = require('craco-less')
const WebpackBar = require('webpackbar') //进度条
const TerserWebpackPlugin = require('terser-webpack-plugin') //取消console日志打印
const PostCss = require('postcss-pxtorem') //配置rem,pc项目需要注释掉
const HtmlWebpackPlugin = require('html-webpack-plugin') //
const WebpackCDNInject = require('webpack-cdn-inject') // 增加CDN文件插入
const BundleAnalyzerPlugin =
require('webpack-bundle-analyzer').BundleAnalyzerPlugin //打包文件分析:开发环境不需要启动注释掉
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin') //压缩css
//多线程
const HappyPack = require('happypack')
const os = require('os')
const happyThreadPool = HappyPack.ThreadPool({
size: os.cpus().length,
})
const NPM_LIFECYCLE_EVENT = process.env.npm_lifecycle_event //获取package.json中的scripts启动类型
const pageconfig = require(`./pages.config`) //项目配置
module.exports = {
eslint: {
enable: false,
},
webpack: {
//别名
alias: {
'@': path.resolve('src'),
},
plugins: [
new WebpackBar(), //进度条
new HappyPack({
//用id来标识处理那里类文件
id: 'happyBabel',
//如何处理 用法和loader 的配置一样
loaders: [
{
loader: 'babel-loader?cacheDirectory=true',
},
],
//共享进程池
threadPool: happyThreadPool,
//允许 HappyPack 输出日志
verbose: true,
}),
new webpack.DefinePlugin({
//配置引用变量
'process.env': {
PRD_KEY: JSON.stringify(pageconfig.prdKey), //区分产品线
API_URL: JSON.stringify(pageconfig.api), //api 有多个API 单独配置多个
CDN_URL: JSON.stringify(pageconfig.cdn_common), //cdn
},
}),
//new BundleAnalyzerPlugin(), //打包文件分析:开发环境不需要启动注释掉
],
configure: (webpackConfig, { env, paths }) => {
//重写 webpack 任意配置
paths.appBuild = `./dist` //'dist'
webpackConfig.output = {
...webpackConfig.output,
path: path.resolve(__dirname, `./dist`), // 修改输出文件目录
}
//禁止生产环境生成sourceMap文件
webpackConfig.devtool =
env === 'development' ? 'cheap-module-source-map' : false
webpackConfig.externals = {
jquery: '$',
} // 使用cdn引入
webpackConfig.plugins.push(
new HtmlWebpackPlugin({
template: './public/index.html',
inject: true,
isRem: true, //判断是否需要rem,如果需要改成true
}),
new WebpackCDNInject({
//cdn 有需要的写配置 注意路径配置,HtmlWebpackPlugin和WebpackCDNInject同时使用HtmlWebpackPlugin要在前
head: pageconfig.isShowConsole
? pageconfig.cdn_console.concat(pageconfig.cdn_common)
: pageconfig.cdn_common,
}),
)
webpackConfig.optimization = {
minimize: true,
minimizer: [
new TerserWebpackPlugin({
cache: true,
parallel: true,
sourceMap: false,
terserOptions: {
ecma: undefined,
warnings: false,
parse: {},
compress: {
drop_console: false,
drop_debugger: false, // 移除断点
pure_funcs:
NPM_LIFECYCLE_EVENT.indexOf('build') === 0
? ['console.log']
: '', // 生产环境下移除console
},
},
}),
new OptimizeCssAssetsPlugin({ parallel: true }),
],
splitChunks: {
chunks: 'async',
minSize: 30000,
maxSize: 0,
minChunks: 1,
maxAsyncRequests: 5,
maxInitialRequests: 3,
automaticNameDelimiter: '~',
name: true,
cacheGroups: {
vendors: {
name: 'vendors',
test: /node_modules/,
minChunks: 1,
priority: -20,
},
echarts: {
name: 'echarts',
test: /echarts/,
minChunks: 1,
priority: -10,
},
reactDayPicker: {
name: 'react-day-picker',
test: /react-day-picker/,
minChunks: 1,
priority: -10,
},
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true,
},
},
},
}
return webpackConfig
},
},
babel: {
plugins: [
//按需加载antd
[
'import',
{
libraryName: 'antd-mobile',
style: true,
},
],
],
},
plugins: [
{
//自定义antd样式
plugin: CracoLessPlugin,
options: {
lessLoaderOptions: {
lessOptions: {
modifyVars: {
'@fill-body': '#ffffff',
'@brand-primary': '#1FC926',
'@color-text-base': '#ffffff',
},
javascriptEnabled: true,
},
},
},
},
],
style: {
postcss: {
//设置rem
plugins: [
PostCss({
rootValue: 50,
propList: ['*'],
}),
],
},
},
}
成熟
在开发过程中,我发现了一个痛点,就是来一个新项目就要复制一份基础框架,然后重新安装所有依赖。
我们开发项目的前端页面都是跟着后台服务走的,公司有一个网关系统,主要用来登录和管理菜单,其余系统通过 iframe 的方式嵌入到网关系统,这样就导致后台创建一个新项目,前端也要跟着复制一个新的基础框架,然后写代码,期间研究过乾坤微前端,不太适合公司现有框架。
在复制开发了 10 多个项目后,有点受不了了。决定再次重构公司的脚手架。在这期间公司的 pc 项目也在准备转 react 模式(早期 react 基本上开发的都是移动端项目),然后了解到 antd,并且了解了 antd 推荐的脚手架 Umi,决定用 Umi 再次重构脚手架。这次重构需要解决的最大难点就是一个架子多项目打包。
import { defineConfig } from '@umijs/max'
import { resolve } from 'path'
import routes from './route_config'
import { globalVariable, buildPath, moduleName } from './project.config'
import { DEFAULT_COLOR } from './src/common/constant'
const prdEnv = process.env.NODE_ENV === 'production'
export default defineConfig({
alias: {
'@': resolve(__dirname, 'src'),
_test: resolve(__dirname, 'src/modules/test'),
},
theme: {
'@primary-color': DEFAULT_COLOR,
},
define: globalVariable,
hash: true,
history: {
type: 'hash',
},
icons: { autoInstall: {} },
locale: {
antd: true, // 如果项目依赖中包含 `antd`,则默认为 true
},
outputPath: buildPath,
publicPath: prdEnv ? './' : '/',
routes: [
{ exact: true, path: '/', redirect: '/404' },
{
path: '/',
component: '@/layouts/index',
routes: routes[moduleName],
},
],
tailwindcss: {},
title: '加载中...',
proxy: {
'/api': {
target: 'https://www.api.cn',
changeOrigin: true,
pathRewrite: {
'^/api': '',
},
},
},
})
主要是通过 package.json 的命令来区分环境和所要打包的项目
{
"name": "xiaoliu",
"version": "2.0.0",
"private": true,
"keywords": ["umi", "react", "antd"],
"author": "xiaoliu",
"scripts": {
"analyze:test": "cross-env ANALYZE=1 max build ",
"dev:test": "max build",
"prd:test": "max build",
"start:test": "max dev",
"start": "npm run start:test"
},
"dependencies": {
"@ant-design/compatible": "^5.1.1",
"@ant-design/pro-components": "^2.3.51",
"@dnd-kit/core": "^6.0.8",
"@dnd-kit/sortable": "^7.0.2",
"@dnd-kit/utilities": "^3.2.1",
"@umijs/max": "^4.0.72",
"@wangeditor/editor": "^5.1.23",
"@wangeditor/editor-for-react": "^1.0.6",
"ahooks": "^3.7.8",
"antd": "^5.7.0",
"axios": "^1.4.0",
"classnames": "^2.3.2",
"echarts": "^5.4.2",
"immutable": "^4.3.0",
"mockjs": "^1.1.0",
"nprogress": "^0.2.0",
"query-string": "^8.1.0",
"react-markdown": "^8.0.6",
"react-syntax-highlighter": "^15.5.0",
"rehype-katex": "^6.0.2",
"rehype-raw": "^6.1.1",
"remark-gfm": "^3.0.1",
"remark-math": "^5.1.1"
},
"devDependencies": {
"@iconify-json/ant-design": "^1.1.4",
"@types/echarts": "^4.9.17",
"@types/react": "^18.0.0",
"@types/react-dom": "^18.0.0",
"cross-env": "^7.0.3",
"prettier": "^2.7.1",
"prettier-plugin-organize-imports": "^2",
"prettier-plugin-packagejson": "^2",
"tailwindcss": "^3",
"typescript": "^4.1.2"
}
}
//project.config.ts
//冒号前面表示环境、冒号后面表示打包的项目
// start:test 本地启动
// dev:test 开发环境打包
// prd:test 正式环境打包
const startType = process.env.npm_lifecycle_event!.split(':')[0] // 获取package.json中的scripts启动类型
const moduleName = process.env.npm_lifecycle_event!.split(':')[1] // 获取package.json中的scripts启动模块
/**
* 启动配置
*/
const startConfig: G.Obj = require(`./config/${startType}/pages.config`)
/**
* 构建路径
*/
const buildPath = `./build/${startType}/${moduleName}/dist`
/**
* 功能模块接口所属接口
*/
const modulesAPI = {
test: 'api_test',
}
/**
* 全局变量
*/
const globalVariable = {
'process.env.API_URL': startConfig[modulesAPI[moduleName]], // api 有多个API 单独配置多个
}
export { globalVariable, buildPath, moduleName }
路由配置根据项目的不同分成不同的文件夹,同一个项目的路由都放在一个目录下,统一输出
//route_config
import testRoutes from './test'
//routeConfig 就是关键点,根据命令行冒号后面的内容来判断需要打包哪些路由
const routeConfig = {
test: testRoutes,
}
export default routeConfig
//test
const testRoutes = [
{
exact: true,
path: '/test',
title: '列表',
component: '@/modules/test/list',
},
{
exact: true,
path: '/test/view',
title: '详情',
component: '@/modules/test/view',
},
]
export default testRoutes
src/modules 文件夹,所有的项目都写在这里,根据类型分别定义项目主目录
modules/test1 、 modules/test2,当前项目的代码都写在目录里面。
总结
对于 react 的开发,已经从最初的 class 到现在全面转向了 hooks,今年也融入了 ts,继续在前端努力进步。