你是否對如何構(gòu)建一個(gè)react應(yīng)用得心應(yīng)手呢
create-react-app MyReactApp
npm run start
嚕嚕啦 多簡單主巍;
but 如果我要改一些配置呢?too young, too simple 能難倒我挪凑?
npm run eject 反編譯create-react-app的config到項(xiàng)目中
npm install react-app-rewired --save 了解下孕索?
好好好,但是您能不能自己寫一個(gè)呢躏碳?
搞旭。。。ののの
其實(shí)代碼我2018年3月份就寫好了肄渗,之后每天忙于寫業(yè)務(wù)代碼(-?-;)
package.json
{
"name": "react-cli",
"version": "0.0.1",
"description": "React 16.0 boilerplate with react-router-dom, redux & webpack 4.x",
"scripts": {
"start": "webpack-dev-server --config build/webpack.dev.conf.js --progress --watch --colors --profile ",
"build": "webpack -p --mode production --progress --config ./build/webpack.prod.conf.js"
},
"devDependencies": {
"@babel/core": "^7.0.0",
"@babel/plugin-syntax-dynamic-import": "^7.0.0",
"@babel/plugin-syntax-object-rest-spread": "^7.0.0",
"@babel/plugin-transform-runtime": "^7.0.0",
"@babel/preset-env": "^7.0.0",
"@babel/preset-react": "^7.0.0",
"@babel/runtime": "^7.0.0",
"@commitlint/cli": "^7.0.0",
"@commitlint/config-conventional": "^7.0.1",
"autoprefixer": "^9.0.0",
"babel-core": "^6.26.3",
"babel-eslint": "^8.2.6",
"babel-loader": "^8.0.2",
"babel-plugin-dynamic-import-node": "^2.2.0",
"babel-plugin-import": "^1.8.0",
"babel-plugin-module-resolver": "^3.1.1",
"clean-webpack-plugin": "^0.1.19",
"compression-webpack-plugin": "^1.1.11",
"copy-webpack-plugin": "^4.5.2",
"cross-env": "^5.2.0",
"css-loader": "^1.0.0",
"file-loader": "^2.0.0",
"friendly-errors-webpack-plugin": "^1.7.0",
"highcharts": "^6.1.2",
"highcharts-react-official": "highcharts/highcharts-react",
"html-webpack-plugin": "^3.2.0",
"husky": "^0.14.3",
"jest": "^23.4.1",
"less": "^3.8.1",
"less-loader": "^4.1.0",
"lint-staged": "^7.2.2",
"mini-css-extract-plugin": "^0.4.1",
"node-notifier": "^5.2.1",
"node-ssh": "^5.1.2",
"portfinder": "^1.0.20",
"postcss-import": "^11.1.0",
"postcss-loader": "^2.1.6",
"postcss-safe-parser": "^4.0.1",
"prettier": "^1.14.2",
"source-map-loader": "^0.2.3",
"style-loader": "^0.21.0",
"styled-components": "^3.4.5",
"url-loader": "^1.0.1",
"webpack": "^4.16.1",
"webpack-bundle-analyzer": "^2.13.1",
"webpack-cli": "^3.1.0",
"webpack-dev-server": "^3.1.4",
"webpack-merge": "^4.2.1"
},
"dependencies": {
"@babel/plugin-proposal-class-properties": "^7.4.0",
"@babel/plugin-proposal-decorators": "^7.4.0",
"@babel/plugin-proposal-do-expressions": "^7.2.0",
"@babel/plugin-proposal-export-default-from": "^7.2.0",
"@babel/plugin-proposal-export-namespace-from": "^7.2.0",
"@babel/plugin-proposal-function-bind": "^7.2.0",
"@babel/plugin-proposal-function-sent": "^7.2.0",
"@babel/plugin-proposal-logical-assignment-operators": "^7.2.0",
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.2.0",
"@babel/plugin-proposal-numeric-separator": "^7.2.0",
"@babel/plugin-proposal-optional-chaining": "^7.2.0",
"@babel/plugin-proposal-pipeline-operator": "^7.3.2",
"@babel/plugin-proposal-throw-expressions": "^7.2.0",
"@babel/plugin-syntax-import-meta": "^7.2.0",
"antd": "^3.7.0",
"axios": "^0.18.0",
"babel-polyfill": "^6.26.0",
"bignumber": "^1.1.0",
"debug": "^4.1.1",
"import-local": "^2.0.0",
"js-base64": "^2.5.1",
"js-cookie": "^2.2.0",
"jsencrypt": "^3.0.0-rc.1",
"memoize-one": "^5.0.1",
"node-sass": "^4.11.0",
"nzh": "^1.0.4",
"pnp-webpack-plugin": "^1.4.1",
"progress-bar-webpack-plugin": "^1.12.1",
"react": "^16.4.1",
"react-dom": "^16.4.1",
"react-loadable": "^5.5.0",
"react-redux": "^5.0.7",
"react-router-config": "^5.1.1",
"react-router-dom": "^4.3.1",
"react-table": "^6.9.2",
"react-transition-group": "^4.4.1",
"redux": "^4.0.0",
"redux-devtools-extension": "^2.13.8",
"redux-logger": "^3.0.6",
"redux-thunk": "^2.3.0",
"sass-loader": "^7.1.0",
"yargs": "^13.2.2"
}
}
.babelrc
{
"presets": [
"@babel/preset-env",
"@babel/preset-react"
],
"plugins": [
"@babel/plugin-syntax-dynamic-import",
"@babel/plugin-transform-runtime",
"dynamic-import-node",
[
"@babel/plugin-proposal-decorators",
{
"legacy": true
}
],
"@babel/plugin-proposal-class-properties",
"@babel/plugin-syntax-import-meta",
"@babel/plugin-proposal-json-strings",
"@babel/plugin-proposal-function-sent",
"@babel/plugin-proposal-export-namespace-from",
"@babel/plugin-proposal-numeric-separator",
"@babel/plugin-proposal-throw-expressions",
"@babel/plugin-proposal-export-default-from",
"@babel/plugin-proposal-logical-assignment-operators",
"@babel/plugin-proposal-optional-chaining",
[
"@babel/plugin-proposal-pipeline-operator",
{
"proposal": "minimal"
}
],
"@babel/plugin-proposal-nullish-coalescing-operator",
"@babel/plugin-proposal-do-expressions",
"@babel/plugin-proposal-function-bind",
]
}
webpack.base.conf.js
'use strict'
const path = require('path');
const PnpWebpackPlugin = require('pnp-webpack-plugin');
function resolve (dir) {
return path.join(__dirname, '..', dir)
}
module.exports = {
context: path.resolve(__dirname, '../'),
entry: {
index: ['babel-polyfill','./src/index.js']
},
output: {
path: path.resolve(__dirname, '../dist'),
filename: 'app.[hash:8].js',
publicPath: '/',
libraryTarget: 'umd',
},
resolve: {
// 自動(dòng)解析文件擴(kuò)展名(補(bǔ)全文件后綴)(從左->右)
extensions: ['.js', '.jsx', '.json'],
alias: {
'@': resolve('src')
},
plugins: [
// 正確解析程序所需的依賴的插件
PnpWebpackPlugin,
],
},
resolveLoader: {
plugins: [
// 也與插件‘n’Play有關(guān)镇眷,但這一次它告訴Webpack從當(dāng)前的包中加載其加載程序
PnpWebpackPlugin.moduleLoader(module),
],
},
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
enforce: 'pre',
use: [{
loader: 'babel-loader',
}]
},
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
loader: 'url-loader',
options: {
// 不寫fallback,file size 大于limit時(shí)翎嫡,會(huì)自動(dòng)調(diào)用file-loader欠动,但是使用的是默認(rèn)的[hash].[ext]無法指定路徑名和文件名
// 顯示聲明 fallback之后,就可以將name傳至file-loader惑申。
// 因?yàn)槭褂昧薈opyWebpackPlugin具伍,防止諸如[name][hash].[ext]和[name].[ext]文件同時(shí)存在
// 特將文件名指定為[name].[ext]
fallback: 'file-loader',
name: 'assets/img/[name].[ext]',
limit: 10000,
}
},
{
test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
loader: 'url-loader',
options: {
fallback: 'file-loader',
limit: 10000,
name: 'assets/media/[name].[ext]',
}
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
loader: 'url-loader',
options: {
fallback: 'file-loader',
limit: 10000,
name: 'assets/fonts/[name].[ext]',
}
}
]
},
node: {
module: 'empty',
dgram: 'empty',
dns: 'mock',
fs: 'empty',
net: 'empty',
tls: 'empty',
child_process: 'empty',
},
performance: false,
}
webpack.dev.conf.js
const utils = require('./utils')
const webpack = require('webpack')
const merge = require('webpack-merge')
const path = require('path')
const baseWebpackConfig = require('./webpack.base.conf')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')
const portfinder = require('portfinder')
const HOST = process.env.HOST
const PORT = (process.env.PORT && Number(process.env.PORT)) || '8080'
const devWebpackConfig = merge(baseWebpackConfig, {
mode: 'development',
optimization: {
minimize: false,
minimizer: [],
splitChunks: {
chunks: 'all',
name:'vendor'
},
},
module: {
//合并styleloader
rules: utils.styleLoaders({ sourceMap: true, usePostCSS: false, extract: false, })
},
// 源錯(cuò)誤檢查
devtool: 'eval-source-map',
devServer: {
clientLogLevel: 'warning',
//在開發(fā)單頁應(yīng)用時(shí)非常有用,它依賴于HTML5 history API圈驼,如果設(shè)置為true人芽,所有的跳轉(zhuǎn)將指向index.html
historyApiFallback: true,
contentBase: path.resolve(__dirname, '../src'),
compress: true,
// 熱加載
hot: true,
//自動(dòng)刷新
inline: true,
//自動(dòng)打開瀏覽器
open: false,
host: HOST||'localhost',
port: PORT,
// 在瀏覽器上全屏顯示編譯的errors或warnings。
overlay: { warnings: false, errors: true },
publicPath: '/',
// 終端輸出的只有初始啟動(dòng)信息绩脆。webpack 的警告和錯(cuò)誤是不輸出到終端的
proxy: {},
quiet: true,
// 通過傳遞 true 開啟 polling啼肩,或者指定毫秒為單位進(jìn)行輪詢。默認(rèn)為false
watchOptions: {
poll: false
}
},
plugins: [
new webpack.DefinePlugin({
...process.env
}),
//開啟HMR(熱替換功能,替換更新部分,不重載頁面衙伶!)
new webpack.HotModuleReplacementPlugin(),
//顯示模塊相對路徑
// new webpack.NamedModulesPlugin(),
//不顯示錯(cuò)誤信息
new webpack.NoEmitOnErrorsPlugin(),
// https://github.com/ampedandwired/html-webpack-plugin
//配置html入口信息
new HtmlWebpackPlugin({
filename: 'index.html',
template: path.resolve(__dirname, '../index.html'),
chunksSortMode: 'none',
inject: true
}),
new CopyWebpackPlugin([{
from: path.resolve(__dirname, '../static'),
to: 'static',
ignore: ['.*']
}]),
]
})
module.exports = new Promise((resolve, reject) => {
// 獲取當(dāng)前設(shè)定的端口
portfinder.basePort = PORT
portfinder.getPort((err, port) => {
if (err) {
reject(err)
} else {
// 發(fā)布新的端口祈坠,對于e2e測試
process.env.PORT = port
devWebpackConfig.devServer.port = port
// 友好的報(bào)錯(cuò)插件
devWebpackConfig.plugins.push(new FriendlyErrorsPlugin({
compilationSuccessInfo: {
messages: [`Your application is running here: http://${devWebpackConfig.devServer.host}:${port}`],
},
onErrors: utils.createNotifierCallback(),
}))
resolve(devWebpackConfig)
}
})
})
webpack.prod.conf.js
'use strict'
const path = require('path')
const utils = require('./utils')
const webpack = require('webpack')
const merge = require('webpack-merge')
const baseWebpackConfig = require('./webpack.base.conf')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const CleanWebpackPlugin = require('clean-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const BundleAnalyzerPlugin = process.env.NODE_ENV=== 'analysis' ? require('webpack-bundle-analyzer').BundleAnalyzerPlugin:null
const CopyWebpackPlugin = require('copy-webpack-plugin')
const env = process.env.NODE_ENV === 'testing'
? {NODE_ENV: '"testing"'}
: {NODE_ENV: '"production"'}
const webpackConfig = merge(baseWebpackConfig, {
mode: 'production',
module: {
//并入style-loader
rules: utils.styleLoaders({
sourceMap: false,
extract: true,
usePostCSS: true
})
},
output: {
path: path.resolve(__dirname, '../dist'),
filename: ('js/[name].[hash:8].js'),
chunkFilename: ('js/[name]-[id].[hash:8].js')
},
//4.0配置 重點(diǎn)
optimization: {
//它的作用是將包含chunks 映射關(guān)系的 list單獨(dú)從 app.js里提取出來,
//因?yàn)槊恳粋€(gè) chunk 的 id 基本都是基于內(nèi)容 hash 出來的矢劲,
//所以你每次改動(dòng)都會(huì)影響它赦拘,如果不將它提取出來的話,等于app.js每次都會(huì)改變芬沉。緩存就失效了躺同。
runtimeChunk: {
name: "manifest"
},
splitChunks: {
chunks: 'all'
}
// splitChunks: {
// //默認(rèn)作用于異步chunk,值為all/initial/async/function(chunk),值為function時(shí)第一個(gè)參數(shù)為遍歷所有入口chunk時(shí)的chunk模塊丸逸,chunk._modules為gaichunk所有依賴的模塊蹋艺,通過chunk的名字和所有依賴模塊的resource可以自由配置,會(huì)抽取所有滿足條件chunk的公有模塊,以及模塊的所有依賴模塊黄刚,包括css
// chunks: "async”,
// //默認(rèn)值是30kb
// minSize: 30000,
// //被多少模塊共享
// minChunks: 1,
// //所有異步請求不得超過5個(gè)
// maxAsyncRequests: 5,
// //初始化并行請求不得超過3個(gè)
// maxInitialRequests: 3,
// //打包后的名稱捎谨,默認(rèn)是chunk的名字通過分隔符(默認(rèn)是~)分隔開,如vendor~
// name: true,
// //設(shè)置緩存組用來抽取滿足不同規(guī)則的chunk,下面以生成common為例
// cacheGroups: {
// common: {
// //抽取的chunk的名字
// name: 'common',
// //同外層的參數(shù)配置憔维,覆蓋外層的chunks涛救,以chunk為維度進(jìn)行抽取
// chunks(chunk) {
// },
// //可以為字符串,正則表達(dá)式业扒,函數(shù)检吆,以module為維度進(jìn)行抽取,只要是滿足條件的module都會(huì)被抽取到該common的chunk中程储,為函數(shù)時(shí)第一個(gè)參數(shù)是遍歷到的每一個(gè)模塊蹭沛,第二個(gè)參數(shù)是每一個(gè)引用到該模塊的chunks數(shù)組臂寝。自己嘗試過程中發(fā)現(xiàn)不能提取出css,待進(jìn)一步驗(yàn)證摊灭。
// test(module, chunks) {
// },
// //優(yōu)先級咆贬,一個(gè)chunk很可能滿足多個(gè)緩存組,會(huì)被抽取到優(yōu)先級高的緩存組中
// priority: 10,
// //最少被幾個(gè)chunk引用
// minChunks: 2,
// // 如果該chunk中引用了已經(jīng)被抽取的chunk斟或,直接引用該chunk素征,不會(huì)重復(fù)打包代碼
// reuseExistingChunk: true,
// // 如果cacheGroup中沒有設(shè)置minSize萝挤,則據(jù)此判斷是否使用上層的minSize御毅,true:則使用0,false:使用上層minSize
// enforce: true
// }
// }
// }
},
plugins: [
//清除目錄
new CleanWebpackPlugin(path.resolve(__dirname, '../dist/*'), {
root: path.resolve(__dirname, '../'),
verbose: true,
dry: false
}),
//允許創(chuàng)建一個(gè)在編譯時(shí)可以配置的全局常量怜珍。這可能會(huì)對開發(fā)模式和發(fā)布模式的構(gòu)建允許不同的行為非常有用
new webpack.DefinePlugin({
'process.env': env
}),
//壓縮css
new MiniCssExtractPlugin({
filename: 'css/[name].[hash:8].css',
chunkFilename: 'css/[name]-[id].[hash:8].css',
}),
new HtmlWebpackPlugin({
filename: 'index.html',
template: path.resolve(__dirname, '../index.html'),
title: 'React Demo',
inject: true, // true->'head' || false->'body'
minify: {
//刪除Html注釋
removeComments: true,
//去除空格
collapseWhitespace: true,
//去除屬性引號
removeAttributeQuotes: true
},
chunksSortMode: 'dependency'
}),
// 該插件會(huì)根據(jù)模塊的相對路徑生成一個(gè)四位數(shù)的hash作為模塊id, 建議用于生產(chǎn)環(huán)境端蛆。
new webpack.HashedModuleIdsPlugin(),
// 這個(gè)插件會(huì)在 webpack 中實(shí)現(xiàn)以上的預(yù)編譯功能 提升你的代碼在瀏覽器中的執(zhí)行速度。
new webpack.optimize.ModuleConcatenationPlugin(),
//把靜態(tài)資源copy
new CopyWebpackPlugin([{
from: path.resolve(__dirname, '../static'),
to: '/static',
ignore: ['.*']
}]),
process.env.NODE_ENV=== 'analysis' ? new BundleAnalyzerPlugin() : ()=>{}
]
})
// 需要服務(wù)端做相關(guān)的gzip配置
/*
gzip on;
gzip_disable "msie6";
gzip_buffers 32 4k;
gzip_static on;
*/
// 頁面請求后的Response Headers中的Content-Encoding的值為“gzip”酥泛,Request Headers中Accept-Encoding的值存在“gzip”值
if (true) {
const CompressionWebpackPlugin = require('compression-webpack-plugin')
webpackConfig.plugins.push(
new CompressionWebpackPlugin({
asset: '[path].gz[query]',
algorithm: 'gzip',
test: /\.(js|css)$/,
threshold: 10240,
minRatio: 0.8
})
)
}
module.exports = webpackConfig
utils.js
'use strict'
const path = require('path')
const autoprefixer = require('autoprefixer');
const packageConfig = require('../package.json')
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
exports.cssLoaders = function (options) {
options = options || {}
// options是用來傳遞參數(shù)給loader的
// minimize表示壓縮今豆,如果是生產(chǎn)環(huán)境就壓縮css代碼
const cssLoader = {
loader: 'css-loader',
options: {
sourceMap: options.sourceMap,
importLoaders: 2,
minimize: options.extract ? true : false,
}
}
//postcss-loader
const postcssLoader = {
loader: 'postcss-loader',
options: {
sourceMap: options.sourceMap,
plugins: () => [autoprefixer({
browsers: 'last 5 versions'
})],
}
}
function generateLoaders (loader, loaderOptions) {
// 將上面的基礎(chǔ)cssLoader配置放在一個(gè)數(shù)組里面
const loaders = options.usePostCSS ? [cssLoader, postcssLoader] : [cssLoader]
// 如果該函數(shù)傳遞了單獨(dú)的loader就加到這個(gè)loaders數(shù)組里面,這個(gè)loader可能是less,sass之類的
if (loader) {
loaders.push({
// 加載對應(yīng)的loader
loader: loader + '-loader',
// Object.assign是es6的方法柔袁,主要用來合并對象的呆躲,淺拷貝
options: Object.assign({}, loaderOptions, {
sourceMap: options.sourceMap
})
})
}
// 注意這個(gè)extract是自定義的屬性,
//可以定義在options里面捶索,主要作用就是當(dāng)配置為true就把文件單獨(dú)提取插掂,false表示不單獨(dú)提取,
if (options.extract) {
return [ MiniCssExtractPlugin.loader ].concat(loaders)
} else {
return ['style-loader'].concat(loaders)
}
}
return {
css: generateLoaders(),
postcss: generateLoaders(),
less: generateLoaders('less', {
modifyVars: {
"@primary-color": "#075DA5",
"@font-size-base": "12px",
'@menu-dark-bg':'#283142'
},
javascriptEnabled: true,
}),
sass: generateLoaders('sass', { indentedSyntax: true }),
scss: generateLoaders('sass'),
stylus: generateLoaders('stylus'),
styl: generateLoaders('stylus')
}
}
// 下面這個(gè)主要處理import這種方式導(dǎo)入的文件類型的打包腥例,上面的exports.cssLoaders是為這一步服務(wù)的
exports.styleLoaders = function (options) {
const output = []
const loaders = exports.cssLoaders(options)
// 下面就是生成的各種css文件的loader對象
for (const extension in loaders) {
// 把每一種文件的laoder都提取出來
const loader = loaders[extension]
// 把最終的結(jié)果都push到output數(shù)組中辅甥,大事搞定
output.push({
test: new RegExp('\\.' + extension + '$'),
// exclude: /node_modules/,
use: loader
});
// 處理node_modules中的樣式問題
// if (options.extract === ) {
// output.push({
// test: new RegExp('\\.' + extension + '$'),
// include: /node_modules/,
// use: loader
// })
// }
}
// console.log(output)
return output
}
exports.createNotifierCallback = () => {
const notifier = require('node-notifier')
return (severity, errors) => {
if (severity !== 'error') return
const error = errors[0]
const filename = error.file && error.file.split('!').pop()
notifier.notify({
title: packageConfig.name,
message: severity + ': ' + error.name,
subtitle: filename || '',
icon: path.join(__dirname, 'logo.png')
})
}
}
至此一個(gè)基于webpack4+bable7的react腳手架就搭建完成了。
這樣的一個(gè)腳手架是不是維護(hù)起來特別方便和簡單呢燎竖?
現(xiàn)在是webpack5了, 后面有時(shí)間給改成webpack5