都说create-react-app是业界最优秀的 React 应用开发工具之一。But,webpack4都更新到v4.20.2 它居然~还没升级,简直~不能忍
看到webpack这更新速度,本人慌得一批,刚好抽空搭建react-andt-mobx脚手架准备升级~
So,在此分享一下升级攻略,收好不谢!
01 安装
npm install -g create-react-app
02 创建应用
//create-react-app是全局命令来创建react项目
create-react-app react-demo
03 自定义webpack配置
npm run eject //自定义模式,暴露出webpack配置,不可逆
04 着手自定义webpack配置
1、目标结构
当然webpack升级准备,调整create-react-app的目录结构已符合我们项目开发的规范是必不可少的。这里重点需关注的为build目录下的一下文件:
paths文件更改打包路经更改:
在项目开发的过程中host配置以及proxy代理是常见的配置,在create-react-app中配置在package.json配置下,灵活性相对不太好,提取webpack中server.js配置:
别忘了修改webpackDevServer.config.js下引用host及proxy下的引用哦。
此时,目录改造全部完毕
渐入佳境,赶紧进入正题
2、webpack3升级webpack4
webpack4新出了一个mode模式,有三种选择,none
,development
,production
.最直观的感受就是你可以少些很多配置,因为一旦你开启了mode模式,webpack4就会给你设置很多基本的东西。
development模式下,将侧重于功能调试和优化开发体验,包含如下内容:
浏览器调试工具
开发阶段的详细错误日志和提示
快速和优化的增量构建机制
production模式下,将侧重于模块体积优化和线上部署,包含如下内容:
开启所有的优化代码
更小的bundle大小
去除掉只在开发阶段运行的代码
Scope hoisting和Tree-shaking
自动启用uglifyjs对代码进行压缩
话不多说,下安装:yarn add webpack webpack-cli webpack-dev-server
这3个包是webpack4的基础功能
webpack 在 webpack 4 里将命令行相关的都迁移至 webpack-cli 包
webpack-dev-server为实时监控文件变化包
安装完成之后,请保持淡定,
得先运行一下,万一直接能打包呢
或许它偷偷做了兼容处理呢,
梦想还是要有呢,虽然...
Plugin could not be registered at 'html-webpack-plugin-before-html-processing'. Hook was not found. BREAKING CHANGE: There need to exist a hook at 'this.hooks'. To create a compatiblity layer for this hook, hook into 'this._pluginCompat'.
果然还是熟悉的味道
上面这个问题是HtmlWebpackPlugin 和 react-dev-utils/InterpolateHtmlPlugin 先后顺序问题,调整下他们的顺序
new HtmlWebpackPlugin({ inject: true, template: paths.appHtml, chunksSortMode: 'none', }), new InterpolateHtmlPlugin(env.raw),
嗯,不出意外的话,搞定
再跑一下代码,这个时候可能就出现了一些百度不到问题,你需要升级各种loader了,
less-loader,sass-loader style-loader url-loader
具体命令:
yarn add less-loader@next
和上面的命令相同,依次升级,运行代码,查看报错,缺啥补啥,成功的选择,值得拥有....
需要升级的有:
html-webpack-plugin
react-dev-utils
修改代码完整篇 webpack.config.dev.js:
const path = require('path'); const webpack = require('webpack'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const CaseSensitivePathsPlugin = require('case-sensitive-paths-webpack-plugin'); const InterpolateHtmlPlugin = require('react-dev-utils/InterpolateHtmlPlugin'); const WatchMissingNodeModulesPlugin = require('react-dev-utils/WatchMissingNodeModulesPlugin'); const eslintFormatter = require('react-dev-utils/eslintFormatter'); const ModuleScopePlugin = require('react-dev-utils/ModuleScopePlugin'); const getClientEnvironment = require('./env'); const paths = require('./paths'); function resolve (dir) { return path.join(__dirname, '..', dir) } const publicPath = '/'; const publicUrl = ''; const env = getClientEnvironment(publicUrl); module.exports = { mode: 'development', devtool: 'cheap-module-source-map', entry: [ require.resolve('./polyfills'), require.resolve('react-dev-utils/webpackHotDevClient'), paths.appIndexJs, ], output: { pathinfo: true, filename: 'static/js/bundle.js', chunkFilename: 'static/js/[name].chunk.js', publicPath: publicPath, devtoolModuleFilenameTemplate: info => path.resolve(info.absoluteResourcePath).replace(/\\/g, '/'), }, resolve: { modules: ['node_modules', paths.appNodeModules].concat( process.env.NODE_PATH.split(path.delimiter).filter(Boolean) ), extensions: ['.web.js', '.mjs', '.js', '.json', '.web.jsx', '.jsx','.less','.scss'], alias: { '@': resolve('src'), 'public': resolve('src/public'), 'components': resolve('src/components'), 'pages': resolve('src/pages'), 'api': resolve('src/api'), 'mock': resolve('src/public/mock'), }, plugins: [ new ModuleScopePlugin(paths.appSrc, [paths.appPackageJson]), ], }, module: { strictExportPresence: true, rules: [ { test: /\.(js|jsx|mjs)$/, enforce: 'pre', use: [ { options: { formatter: eslintFormatter, eslintPath: require.resolve('eslint'), }, loader: require.resolve('eslint-loader'), }, ], include: paths.appSrc, }, { oneOf: [ { test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/], loader: require.resolve('url-loader'), options: { limit: 10000, name: 'static/media/[name].[hash:8].[ext]', }, }, { test: /\.(js|jsx|mjs)$/, include: paths.appSrc, loader: require.resolve('babel-loader'), options: { cacheDirectory: true, } }, { test: /\.(css|less)$/, use: [ require.resolve('style-loader'), { loader: require.resolve('css-loader'), options: { importLoaders: 1, }, }, { loader: require.resolve('postcss-loader'), options: { ident: 'postcss', plugins: () => [ require('postcss-flexbugs-fixes'), autoprefixer({ browsers: [ '>1%', 'last 4 versions', 'Firefox ESR', 'not ie < 9', // React doesn't support IE8 anyway ], flexbox: 'no-2009', }), ], }, }, { loader: require.resolve('less-loader') // compiles Less to CSS }, ], }, { test: /\.(css|scss)$/, use: [ require.resolve('style-loader'), { loader: require.resolve('css-loader'), options: { importLoaders: 1, }, }, { loader: require.resolve('postcss-loader'), options: { ident: 'postcss', plugins: () => [ require('postcss-flexbugs-fixes'), autoprefixer({ browsers: [ '>1%', 'last 4 versions', 'Firefox ESR', 'not ie < 9', // React doesn't support IE8 anyway ], flexbox: 'no-2009', }), ], }, }, { loader: require.resolve('sass-loader') // compiles Less to CSS }, ], }, { exclude: [/\.(js|jsx|mjs)$/,/\.(css|less)$/, /\.html$/, /\.json$/], loader: require.resolve('file-loader'), options: { name: 'static/media/[name].[hash:8].[ext]', }, }, ], }, ], }, plugins: [ new HtmlWebpackPlugin({ inject: true, template: paths.appHtml, chunksSortMode: 'none', }), new InterpolateHtmlPlugin(env.raw), new webpack.DefinePlugin(env.stringified), new webpack.HotModuleReplacementPlugin(), new CaseSensitivePathsPlugin(), new WatchMissingNodeModulesPlugin(paths.appNodeModules), new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/), ], node: { dgram: 'empty', fs: 'empty', net: 'empty', tls: 'empty', child_process: 'empty', }, performance: { hints: false, }, optimization: { namedModules: true, nodeEnv: 'development', }, };
还需注意的是webpack4对ExtractTextWebpackPlugin做了调整,建议选用新的CSS文件提取插件mini-css-extract-plugin。生产环境下我们需要做一下配置调整:
webpack.config.prod.js
const autoprefixer = require('autoprefixer'); const path = require('path'); const webpack = require('webpack'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const ManifestPlugin = require('webpack-manifest-plugin'); const InterpolateHtmlPlugin = require('react-dev-utils/InterpolateHtmlPlugin'); const SWPrecacheWebpackPlugin = require('sw-precache-webpack-plugin'); const eslintFormatter = require('react-dev-utils/eslintFormatter'); const ModuleScopePlugin = require('react-dev-utils/ModuleScopePlugin'); const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const UglifyJsPlugin = require("uglifyjs-webpack-plugin"); const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin"); const paths = require('./paths'); const getClientEnvironment = require('./env'); const theme = require('../antd-theme.js'); function resolve (dir) { return path.join(__dirname, '..', dir) } const publicPath = paths.servedPath; const shouldUseSourceMap = process.env.GENERATE_SOURCEMAP !== 'false'; const publicUrl = publicPath.slice(0, -1); const env = getClientEnvironment(publicUrl); if (env.stringified['process.env'].NODE_ENV !== '"production"') { throw new Error('Production builds must have NODE_ENV=production.'); } module.exports = { mode: "production", bail: true, devtool: shouldUseSourceMap ? 'source-map' : false, entry: [require.resolve('./polyfills'), paths.appIndexJs], output: { path: paths.appBuild, filename: 'static/js/[name].[chunkhash:8].js', chunkFilename: 'static/js/[name].[chunkhash:8].chunk.js', publicPath: publicPath, devtoolModuleFilenameTemplate: info => path .relative(paths.appSrc, info.absoluteResourcePath) .replace(/\\/g, '/'), }, resolve: { modules: ['node_modules', paths.appNodeModules].concat( // It is guaranteed to exist because we tweak it in `env.js` process.env.NODE_PATH.split(path.delimiter).filter(Boolean) ), extensions: ['.web.js', '.mjs', '.js', '.json', '.web.jsx', '.jsx','.less'], alias: { '@': resolve('src'), 'public': resolve('src/public'), 'components': resolve('src/components'), 'pages': resolve('src/pages'), 'mock': resolve('src/public/mock'), 'api': resolve('src/api'), 'react-native': 'react-native-web', }, plugins: [ new ModuleScopePlugin(paths.appSrc, [paths.appPackageJson]), ], }, module: { strictExportPresence: true, rules: [ { test: /\.(js|jsx|mjs)$/, enforce: 'pre', use: [ { options: { formatter: eslintFormatter, eslintPath: require.resolve('eslint'), }, loader: require.resolve('eslint-loader'), }, ], include: paths.appSrc, }, { oneOf: [ { test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/], loader: require.resolve('url-loader'), options: { limit: 10000, name: 'static/media/[name].[hash:8].[ext]', }, }, // Process JS with Babel. { test: /\.(js|jsx|mjs)$/, include: paths.appSrc, loader: require.resolve('babel-loader'), options: { plugins: [ ['import', [{ libraryName: 'antd', style: true }]], // import less ], compact: true, }, }, { test: /\.(less|css)$/, use: [ MiniCssExtractPlugin.loader, "css-loader", "less-loader?{modifyVars:" + JSON.stringify(theme) + "}" ], }, { test: /\.(scss|sass)$/, use: [ MiniCssExtractPlugin.loader, "css-loader", "sass-loader" ] }, { loader: require.resolve('file-loader'), exclude: [/\.(js|jsx|mjs)$/,/\.(css|less)$/, /\.html$/, /\.json$/], options: { name: 'static/media/[name].[hash:8].[ext]', }, }, ], }, ], }, optimization: { runtimeChunk: { name: 'manifest' }, minimize: true, noEmitOnErrors: true, minimizer: [ new UglifyJsPlugin({ cache: true, parallel: true, sourceMap: false }), new OptimizeCSSAssetsPlugin({}) ], splitChunks: { minSize: 30000, maxSize: 3000000, minChunks: 1, maxAsyncRequests: 5, maxInitialRequests: 3, name: true, cacheGroups: { vendor: { chunks: 'initial', name: 'vendor', test: 'vendor' }, echarts: { chunks: 'all', name: 'echarts', test: /[\\/]echarts[\\/]/, } } } }, plugins: [ new HtmlWebpackPlugin({ inject: true, template: paths.appHtml, minify: { removeComments: true, collapseWhitespace: true, removeRedundantAttributes: true, useShortDoctype: true, removeEmptyAttributes: true, removeStyleLinkTypeAttributes: true, keepClosingSlash: true, minifyJS: true, minifyCSS: true, minifyURLs: true, }, }), new InterpolateHtmlPlugin(env.raw), new webpack.DefinePlugin(env.stringified), new webpack.NamedModulesPlugin(), new webpack.optimize.OccurrenceOrderPlugin(true), new MiniCssExtractPlugin({ filename: "css/[name].[hash].css", chunkFilename: "css/[name].[hash].css" }), new ManifestPlugin({ fileName: 'asset-manifest.json', }), new SWPrecacheWebpackPlugin({ dontCacheBustUrlsMatching: /\.\w{8}\./, filename: 'service-worker.js', logger(message) { if (message.indexOf('Total precache size is') === 0) { return; } if (message.indexOf('Skipping static resource') === 0) { return; } console.log(message); }, minify: true, navigateFallback: publicUrl + '/index.html', navigateFallbackWhitelist: [/^(?!\/__).*/], staticFileGlobsIgnorePatterns: [/\.map$/, /asset-manifest\.json$/], }), new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/), ], node: { dgram: 'empty', fs: 'empty', net: 'empty', tls: 'empty', child_process: 'empty', }, };
此时基本完成了webpack4升级的改造
但是运行下,还有个隐形的坑待处理……
因为引入了andt组件库,
高版本对less编译会出错。。
噗~升级了一路,碰到个需要降级处理的,
好,降低less版本 "less": "2.7.3",
运行一波,亲测完美~
最后附package.json
{ "dependencies": { "antd": "^3.9.0-beta.6", "autoprefixer": "7.1.6", "axios": "^0.18.0", "babel-core": "6.26.0", "babel-eslint": "7.2.3", "babel-jest": "20.0.3", "babel-loader": "7.1.2", "babel-plugin-import": "^1.8.0", "babel-polyfill": "^6.26.0", "babel-preset-react-app": "^3.1.1", "babel-runtime": "^6.26.0", "case-sensitive-paths-webpack-plugin": "2.1.1", "chalk": "1.1.3", "classnames": "^2.2.6", "core-decorators": "^0.20.0", "create-keyframe-animation": "^0.1.0", "css-loader": "0.28.7", "dotenv": "4.0.0", "dotenv-expand": "4.2.0", "eslint": "4.10.0", "eslint-config-react-app": "^2.1.0", "eslint-loader": "^2.1.1", "eslint-plugin-flowtype": "2.39.1", "eslint-plugin-import": "2.8.0", "eslint-plugin-jsx-a11y": "5.1.1", "eslint-plugin-react": "7.4.0", "express": "^4.16.3", "fastclick": "^1.0.6", "file-loader": "2.0.0", "fs-extra": "3.0.1", "good-storage": "^1.0.1", "history": "^4.7.2", "html-webpack-plugin": "^3.2.0", "immutable": "^3.8.2", "jest": "20.0.4", "js-base64": "^2.4.3", "jsonp": "^0.2.1", "less": "2.7.3", "less-loader": "^4.0.1", "lyric-parser": "^1.0.1", "mini-css-extract-plugin": "^0.4.3", "mobx": "^4.1.1", "mobx-react": "^5.0.0", "mobx-react-devtools": "^5.0.1", "node-sass": "^4.9.3", "object-assign": "4.1.1", "optimize-css-assets-webpack-plugin": "^5.0.1", "postcss-flexbugs-fixes": "3.2.0", "postcss-loader": "2.0.8", "promise": "8.0.1", "prop-types": "^15.6.1", "raf": "3.4.0", "react": "^16.3.0", "react-addons-css-transition-group": "^15.6.2", "react-dev-utils": "^6.0.0-next.a671462c", "react-dom": "^16.3.0", "react-hot-loader": "^4.3.4", "react-lazyload": "^2.3.0", "react-loadable": "^5.5.0", "react-router-dom": "^4.2.2", "react-transition-group": "^2.3.1", "sass-loader": "^7.1.0", "style-loader": "0.19.0", "sw-precache-webpack-plugin": "^0.11.5", "uglifyjs-webpack-plugin": "^2.0.1", "url-loader": "0.6.2", "webpack": "^4.19.0", "webpack-cli": "^3.1.0", "webpack-dev-server": "^3.1.8", "webpack-manifest-plugin": "^2.0.4", "whatwg-fetch": "2.0.3" }, "scripts": { "start": "node scripts/start.js", "build": "node scripts/build.js" }, "babel": { "presets": [ "react-app" ], "plugins": [ "transform-decorators-legacy" ] }, "eslintConfig": { "extends": "react-app" }, "devDependencies": { "babel-plugin-transform-decorators-legacy": "^1.3.4", "better-scroll": "^1.9.1" }, }
结 语
前端的框架更新速度,悄无声息又超乎想象,需要不断保持着对前端的热情和主动,研究一波前沿的技术架构和设计理念....
比如参加D2前端技术沙龙~
恍惚中,感觉这一波操作
好像还能再优化~优化~