你是否对如何构建一个react应用得心应手呢
create-react-app MyReactApp
npm run start
噜噜啦 多简单;
but 如果我要改一些配置呢?too young, too simple 能难倒我?
npm run eject 反编译create-react-app的config到项目中
npm install react-app-rewired --save 了解下?
好好好,但是您能不能自己写一个呢?
。。。ののの
其实代码我2018年3月份就写好了,之后每天忙于写业务代码(-。-;)
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: {
// 自动解析文件扩展名(补全文件后缀)(从左->右)
extensions: ['.js', '.jsx', '.json'],
alias: {
'@': resolve('src')
},
plugins: [
// 正确解析程序所需的依赖的插件
PnpWebpackPlugin,
],
},
resolveLoader: {
plugins: [
// 也与插件‘n’Play有关,但这一次它告诉Webpack从当前的包中加载其加载程序
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时,会自动调用file-loader,但是使用的是默认的[hash].[ext]无法指定路径名和文件名
// 显示声明 fallback之后,就可以将name传至file-loader。
// 因为使用了CopyWebpackPlugin,防止诸如[name][hash].[ext]和[name].[ext]文件同时存在
// 特将文件名指定为[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, })
},
// 源错误检查
devtool: 'eval-source-map',
devServer: {
clientLogLevel: 'warning',
//在开发单页应用时非常有用,它依赖于HTML5 history API,如果设置为true,所有的跳转将指向index.html
historyApiFallback: true,
contentBase: path.resolve(__dirname, '../src'),
compress: true,
// 热加载
hot: true,
//自动刷新
inline: true,
//自动打开浏览器
open: false,
host: HOST||'localhost',
port: PORT,
// 在浏览器上全屏显示编译的errors或warnings。
overlay: { warnings: false, errors: true },
publicPath: '/',
// 终端输出的只有初始启动信息。webpack 的警告和错误是不输出到终端的
proxy: {},
quiet: true,
// 通过传递 true 开启 polling,或者指定毫秒为单位进行轮询。默认为false
watchOptions: {
poll: false
}
},
plugins: [
new webpack.DefinePlugin({
...process.env
}),
//开启HMR(热替换功能,替换更新部分,不重载页面!)
new webpack.HotModuleReplacementPlugin(),
//显示模块相对路径
// new webpack.NamedModulesPlugin(),
//不显示错误信息
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) => {
// 获取当前设定的端口
portfinder.basePort = PORT
portfinder.getPort((err, port) => {
if (err) {
reject(err)
} else {
// 发布新的端口,对于e2e测试
process.env.PORT = port
devWebpackConfig.devServer.port = port
// 友好的报错插件
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配置 重点
optimization: {
//它的作用是将包含chunks 映射关系的 list单独从 app.js里提取出来,
//因为每一个 chunk 的 id 基本都是基于内容 hash 出来的,
//所以你每次改动都会影响它,如果不将它提取出来的话,等于app.js每次都会改变。缓存就失效了。
runtimeChunk: {
name: "manifest"
},
splitChunks: {
chunks: 'all'
}
// splitChunks: {
// //默认作用于异步chunk,值为all/initial/async/function(chunk),值为function时第一个参数为遍历所有入口chunk时的chunk模块,chunk._modules为gaichunk所有依赖的模块,通过chunk的名字和所有依赖模块的resource可以自由配置,会抽取所有满足条件chunk的公有模块,以及模块的所有依赖模块,包括css
// chunks: "async”,
// //默认值是30kb
// minSize: 30000,
// //被多少模块共享
// minChunks: 1,
// //所有异步请求不得超过5个
// maxAsyncRequests: 5,
// //初始化并行请求不得超过3个
// maxInitialRequests: 3,
// //打包后的名称,默认是chunk的名字通过分隔符(默认是~)分隔开,如vendor~
// name: true,
// //设置缓存组用来抽取满足不同规则的chunk,下面以生成common为例
// cacheGroups: {
// common: {
// //抽取的chunk的名字
// name: 'common',
// //同外层的参数配置,覆盖外层的chunks,以chunk为维度进行抽取
// chunks(chunk) {
// },
// //可以为字符串,正则表达式,函数,以module为维度进行抽取,只要是满足条件的module都会被抽取到该common的chunk中,为函数时第一个参数是遍历到的每一个模块,第二个参数是每一个引用到该模块的chunks数组。自己尝试过程中发现不能提取出css,待进一步验证。
// test(module, chunks) {
// },
// //优先级,一个chunk很可能满足多个缓存组,会被抽取到优先级高的缓存组中
// priority: 10,
// //最少被几个chunk引用
// minChunks: 2,
// // 如果该chunk中引用了已经被抽取的chunk,直接引用该chunk,不会重复打包代码
// reuseExistingChunk: true,
// // 如果cacheGroup中没有设置minSize,则据此判断是否使用上层的minSize,true:则使用0,false:使用上层minSize
// enforce: true
// }
// }
// }
},
plugins: [
//清除目录
new CleanWebpackPlugin(path.resolve(__dirname, '../dist/*'), {
root: path.resolve(__dirname, '../'),
verbose: true,
dry: false
}),
//允许创建一个在编译时可以配置的全局常量。这可能会对开发模式和发布模式的构建允许不同的行为非常有用
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'
}),
// 该插件会根据模块的相对路径生成一个四位数的hash作为模块id, 建议用于生产环境。
new webpack.HashedModuleIdsPlugin(),
// 这个插件会在 webpack 中实现以上的预编译功能 提升你的代码在浏览器中的执行速度。
new webpack.optimize.ModuleConcatenationPlugin(),
//把静态资源copy
new CopyWebpackPlugin([{
from: path.resolve(__dirname, '../static'),
to: '/static',
ignore: ['.*']
}]),
process.env.NODE_ENV=== 'analysis' ? new BundleAnalyzerPlugin() : ()=>{}
]
})
// 需要服务端做相关的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是用来传递参数给loader的
// minimize表示压缩,如果是生产环境就压缩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) {
// 将上面的基础cssLoader配置放在一个数组里面
const loaders = options.usePostCSS ? [cssLoader, postcssLoader] : [cssLoader]
// 如果该函数传递了单独的loader就加到这个loaders数组里面,这个loader可能是less,sass之类的
if (loader) {
loaders.push({
// 加载对应的loader
loader: loader + '-loader',
// Object.assign是es6的方法,主要用来合并对象的,浅拷贝
options: Object.assign({}, loaderOptions, {
sourceMap: options.sourceMap
})
})
}
// 注意这个extract是自定义的属性,
//可以定义在options里面,主要作用就是当配置为true就把文件单独提取,false表示不单独提取,
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')
}
}
// 下面这个主要处理import这种方式导入的文件类型的打包,上面的exports.cssLoaders是为这一步服务的
exports.styleLoaders = function (options) {
const output = []
const loaders = exports.cssLoaders(options)
// 下面就是生成的各种css文件的loader对象
for (const extension in loaders) {
// 把每一种文件的laoder都提取出来
const loader = loaders[extension]
// 把最终的结果都push到output数组中,大事搞定
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')
})
}
}
至此一个基于webpack4+bable7的react脚手架就搭建完成了。
这样的一个脚手架是不是维护起来特别方便和简单呢?
现在是webpack5了, 后面有时间给改成webpack5