在咱們使用create-react-app
建立react項目的時候,雖然能夠享受一鍵帶來的方便,可是封裝好的不少配置並不能知足多元化項目的需求。咱們知道,不一樣團隊team可能制定有不一樣的eslint規則,或者不一樣的項目結構等等情形。一鍵化的服務有一個通病,就是拓展起來比較麻煩,並且遇到溝坎的時候可能會使你立馬處於舉步維艱的地步,耗時耗力。javascript
最近,恰好接手一個項目遇到了這樣的狀況,因此把項目從建立到啓動所遇到的一些自定義配置在這裏作一下簡單的總結。但願對從此接手的項目開發過程有所助益,提高效率。css
這裏默認省略使用create-react-app
(後面簡稱「CRA」)建立項目的過程,可自行搜網絡查找相關資料學習。html
最初建立的項目結構是這樣的: java
當前的react版本:16.13.1
,當前的webpack版本:4.42.0
,包管理工具:yarn
node
CRA項目構建基於的是react-scripts
包,看不到任何關於eslint和webpack的配置,由於都在這個包裏,這種設計的好處是,能夠跟隨react-scripts
包的升級而升級,很是方便,項目也很是純淨,給開發者開箱即用的體驗。react
可是若是不知足當前業務需求,須要自定義一些功能,那可能就沒什麼體驗了。針對這種狀況,咱們也有對應的策略,須要採起一些方法。目前主要方式有兩種:webpack
yarn eject
。這裏注意必須在項目沒有改動以前執行才能夠,並且是一次性的,不可逆的。package.json
提供了這樣一個命令,執行完會自動刪除掉。{
...
"scripts": {
"eject": "react-scripts eject"
},
...
}
複製代碼
react-app-rewried
和customize-cra
來實現。在不透傳CRA項目包裏配置的前提下,覆蓋和拓展webpack的配置項。這樣比較簡單幹淨,容易管理。後面我會具體介紹。git
執行完yarn eject
,目錄結構會變爲下面所示: github
咱們能夠看到項目多了config
和script
兩個文件夾。這裏就是透傳出來的CRA項目全部的webpack配置。web
package.json
文件裏也會變化,
"scripts": {
"start": "node scripts/start.js",
"build": "node scripts/build.js",
"test": "node scripts/test.js",
...
},
...
"dependencies": {
"@babel/core": "7.9.0",
"@svgr/webpack": "4.3.3",
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.3.2",
"@testing-library/user-event": "^7.1.2",
"@typescript-eslint/eslint-plugin": "^2.10.0",
"@typescript-eslint/parser": "^2.10.0",
"anujs": "^1.6.2",
"babel-eslint": "10.1.0",
"babel-jest": "^24.9.0",
"babel-loader": "8.1.0",
"babel-plugin-named-asset-import": "^0.3.6",
"babel-preset-react-app": "^9.1.2",
"camelcase": "^5.3.1",
"case-sensitive-paths-webpack-plugin": "2.3.0",
"css-loader": "3.4.2",
"dotenv": "8.2.0",
"dotenv-expand": "5.1.0",
"eslint": "^6.6.0",
"eslint-config-react-app": "^5.2.1",
"eslint-loader": "3.0.3",
"eslint-plugin-flowtype": "4.6.0",
"eslint-plugin-import": "2.20.1",
"eslint-plugin-jsx-a11y": "6.2.3",
"eslint-plugin-react": "7.19.0",
"eslint-plugin-react-hooks": "^1.6.1",
"file-loader": "4.3.0",
"fs-extra": "^8.1.0",
"html-webpack-plugin": "4.0.0-beta.11",
"identity-obj-proxy": "3.0.0",
"jest": "24.9.0",
"jest-environment-jsdom-fourteen": "1.0.1",
"jest-resolve": "24.9.0",
"jest-watch-typeahead": "0.4.2",
"mini-css-extract-plugin": "0.9.0",
"node-sass": "^4.14.1",
"optimize-css-assets-webpack-plugin": "5.0.3",
"pnp-webpack-plugin": "1.6.4",
"postcss-flexbugs-fixes": "4.1.0",
"postcss-loader": "3.0.0",
"postcss-normalize": "8.0.1",
"postcss-preset-env": "6.7.0",
"postcss-safe-parser": "4.0.1",
"react": "^16.13.1",
"react-app-polyfill": "^1.0.6",
"react-dev-utils": "^10.2.1",
"react-dom": "^16.13.1",
"resolve": "1.15.0",
"resolve-url-loader": "3.1.1",
"sass-loader": "8.0.2",
"semver": "6.3.0",
"style-loader": "0.23.1",
"terser-webpack-plugin": "2.3.5",
"ts-pnp": "1.1.6",
"url-loader": "2.3.0",
"webpack": "4.42.0",
"webpack-dev-server": "3.11.0",
"webpack-manifest-plugin": "2.2.0",
"workbox-webpack-plugin": "4.3.1"
},
...
"jest": {
"roots": [
"<rootDir>/src"
....
.....
複製代碼
此時,你只須要在config
文件夾的webpack.config.js
等文件裏修改相關配置便可。(這裏eject修改配置的方式,因爲網絡介紹已經不少,再也不贅述)
yarn add react-app-rewried customize-cra -D
複製代碼
替換掉原來的react-scripts
命令
{
...
"scripts": {
"start": "react-app-rewired start",
"build": "react-app-rewried build",
"test": "react-app-rewired test --env=jsdom"
}
...
}
複製代碼
項目根目錄新建文件config-overrides.js
,通常選擇在根目錄建立,你也能夠自定義目錄,這裏不作贅述。
const {
override,
addWebpackAlias,
addLessLoader,
addDecoratorsLegacy,
useEslintRc,
...
} = require('customize-cra');
const customize = () => (config) => {
// 要自定義的配置內容
...
return config;
}
...
module.exports = override(
// 導入配置
....
);
複製代碼
customize-cra
的api
文檔能夠詳細查看:github.com/arackaf/cus…,這裏我只介紹下項目經常使用到的幾個。
// isEnvDevelopment和isEnvProduction用於項目當前環境判斷
...
config.entry = multiConfigtools.allSitePath(isEnvDevelopment);
config.output.filename = isEnvProduction
? 'static/js/[name].[contenthash:8].js'
: isEnvDevelopment && 'static/js/[name].bundle.js';
config.devtool = isEnvProduction ? false : 'source-map';
...
複製代碼
...
isEnvProduction &&
config.plugins.push(
new es3ifyPlugin(),
....
);
...
複製代碼
...
config.plugins = config.plugins.filter(
(p) => p.constructor.name !== 'ManifestPlugin'
);
...
複製代碼
addWebpackAlias({
'react': 'React',
'views': path.resolve(__dirname, 'src/views'),
'@': path.resolve(__dirname, 'src'),
// '@': paths.appSrc
...
}),
複製代碼
// 開啓才能使本地的.eslintrc.js文件生效
useEslintRc('.eslintrc.js'),
複製代碼
默認支持了css
、postcss
和sass
,若是想使用less,必須單獨添加。
yarn add less less-loader -D
複製代碼
而後添加配置
addLessLoader({
lessOptions: {
javascriptEnabled: true,
modifyVars: { '@primary-color': '#777' },
...
localIdentName: '[local]-[hash:base64:8]'
...
},
})
複製代碼
新建.env
文件,添加內容
EXTEND_ESLINT=true
複製代碼
此時默認配置裏就會啓動根目錄裏的eslint配置。 這裏我推薦使用結合prettier
的方式,咱們知道prettier在代碼的美化方面要比eslint更擅長。並且自動格式化的功能,更是開發過程當中的一把利器。
下面介紹下怎麼使用,先執行命令安裝
yarn add eslint-config-prettier eslint-plugin-prettier -D
複製代碼
而後執行
eslint --init
複製代碼
完以後,能夠看到項目根目錄多了一個.eslintrc.js
的文件,接下來就在這個文件裏來添加配置支持prettier
module.exports = {
...
extends: [
'plugin:react/recommended',
'plugin:prettier/recommended',
],
...
plugins: ['prettier'],
...
rules: [
'prettier/prettier': 'error',
...
]
}
複製代碼
配置完之後,咱們再新建.prettierrc
,添加你須要約定的規則內容
{
...
"singleQuote": true,
"printWidth": 120,
...
}
複製代碼
從新啓動項目就生效了。
以前的內容裏也提到了,此時也須要在項目根目錄添加.eslintrc.js
和.prettierrc
兩個配置文件,而後在config-overrides.js
裏添加配置完成後啓用便可。
// 開啓才能使本地的.eslintrc.js文件生效
useEslintRc('.eslintrc.js'),
複製代碼
咱們能夠經過一些工具實現,在提交前prettier
來優化代碼,eslint
校驗代碼。
處理pre-commit
、pre-push
等命令的工具
對git
暫存區代碼,運行linter
的工具
yarn add lint-staged husky -D
複製代碼
{
...
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"src/*.{js,jsx,mjs,ts,tsx}": [
"prettier --write",
"eslint --cache --fix",
"git add"
],
"src/*.{css,scss,less,json,html,md,markdown}": [
"prettier --write"
"git add"
]
}
...
}
複製代碼
接手的這個項目須要打包導出不一樣需求場景的頁面,來提供給後端,此時CRA默認項目就沒法知足了,那麼怎麼解決呢,下面介紹下如何實現
...
├── src
│ ├── components
│ ├── views
│ ├── demo
│ │ ├── demo.html
│ │ └── demo.js
│ └── index
│ ├── index.html
│ └── index.js
...
複製代碼
分別添加不一樣頁面的html
和js
的內容,這裏不作贅述。 2. 修改config目錄下paths.js文件 修改默認入口的路徑
...
appHtml: resolveApp('src/views/index/index.html'),
appIndexJs: resolveModule(resolveApp, 'src/views/index/index'),
...
複製代碼
新建文件multi.config.js
,添加內容
const path = require('path');
const glob = require('glob');
const paths = require('./config/paths');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const allSitePath = (isEnvDevelopment) => {
let entryFiles = glob.sync(paths.appSrc + '/views/*');
let map = {};
entryFiles.forEach((item) => {
let filename = item.substring(item.lastIndexOf('/') + 1);
let filePath = `${item}/${filename}.js`;
map[filename] = [
isEnvDevelopment &&
require.resolve('react-dev-utils/webpackHotDevClient'),
filePath
].filter(Boolean);
});
return map;
}
const htmlPlugin = (isEnvProduction, isEnvDevelopment) => {
let fileNameLists = Object.keys(
allSitePath(isEnvDevelopment)
);
let arr = [];
fileNameLists.forEach(item => {
let filename = item.substring(item.lastIndexOf('/') + 1);
arr.push(
// Generates an `index.html` file with the <script> injected.
new HtmlWebpackPlugin(
Object.assign(
{},
{
inject: true,
filename: item + '.html',
chunks: [item],
template: path.resolve(paths.appSrc, `views/${filename}/${filename}.html`),
},
isEnvProduction
? {
minify: {
removeComments: true,
collapseWhitespace: true,
removeRedundantAttributes: true,
useShortDoctype: true,
removeEmptyAttributes: true,
removeStyleLinkTypeAttributes: true,
keepClosingSlash: true,
minifyJS: true,
minifyCSS: true,
minifyURLs: true,
},
}
: undefined
)
)
);
});
return arr;
}
module.exports = {
allSitePath,
htmlPlugin
}
複製代碼
webpack.config.js
文件修改
...
const multiConfig = require('../multi.config');
...
entry: multiConfig.allSitePath(isEnvDevelopment),
...
...
filename: isEnvProduction
? 'static/js/[name].[contenthash:8].js'
: isEnvDevelopment && 'static/js/[name].bundle.js',
...
...
plugins:[
...multiConfig.htmlPlugin(isEnvProduction, isEnvDevelopment),
...,
...,
]
...
...
// 註釋掉 ManifestPlugin
new ManifestPlugin({s
fileName: 'asset-manifest.json',
....
...
複製代碼
運行yarn build
命令,能夠看到build/
兩個html
文件成功編譯,啓動項目經過不一樣url查看不一樣頁面效果
這種方式更簡單,只須要在config-overrides.js
文件裏作修改便可
...
...
const multiConfig = require('./multi.config');
const paths = require('react-scripts/config/paths');
paths.appHtml = `${paths.appSrc}/views/index/index.html`;
paths.appIndexJs = `${paths.appSrc}/views/index/index.js`;
...
...
// customize裏添加
config.entry = multiConfig.allSitePath(isEnvDevelopment);
config.output.filename = isEnvProduction
? 'static/js/[name].[contenthash:8].js'
: isEnvDevelopment && 'static/js/[name].bundle.js';
config.plugins = [
...multiConfig.htmlPlugin(isEnvProduction, isEnvDevelopment),
];
...
...
複製代碼
運行項目,就能夠看到編譯多個頁面成功。
alias: {
....
'@': path.resolve(__dirname, '../src'),
'react': 'anujs',
....
},
複製代碼
extensions: ['.js', '.es', '.jsx', '.scss', 'less'],
複製代碼
........node_modules/neo-async/async.js:16
throw new Error('Callback was already called.');
...
Error: Callback was already called.
複製代碼
採用eject
方式,修改默認webpack
配置來添加less
支持的時候,遇到了這個報錯仔細分析了下緣由,初步判斷應該是執行順序致使的問題。看了下webpack執行的原理,發現是loader加載的時候,是按照CRA項目默認配置的順序執行,添加less-loader,放在了前面的位置,致使這個報錯,因此less須要添加在sass配置以後,就能夠順利編譯了。
這裏強調一點,就是使用
yarn eject
方式來引入less
配置的時候,最好是要註釋掉sass
的配置,否則會出現打包報錯。因爲項目安裝sass
依賴耗時比較長,網絡情況不佳的狀況下,下載失敗的機率比較高。因此建議,直接用less
取代掉sass
,體驗更高。記得刪掉package.json
裏的sass
依賴哦。
等等
在這個項目的啓動階段,接觸到的自定義配置十分有限,後續隨着業務愈來愈複雜和使用場景愈來愈多,我會不斷實時更新所遇到的問題和解決方案,供你們參考,但願這篇文章對你能有稍許幫助。