Open Loloao opened 3 years ago
安装webpack相关工具:
$ npm i -D webpack webpack-cli webpack-dev-server webpack-merge 在config目录下新建几个文件:config.js, webpack.base.js, webpack.prod.js, webpack.dev.js, build.js
$ npm i -D webpack webpack-cli webpack-dev-server webpack-merge
config
config.js
webpack.base.js
webpack.prod.js
webpack.dev.js
build.js
先抽取一些通用的配置:
// config/config.js const path = require('path'); module.exports = { assetsRoot: path.resolve(__dirname, '../dist'), assetsDirectory: 'static', publicPath: '/', indexPath: path.resolve(__dirname, '../public/index.html'), }; // config/webpack.base.js const path = require('path'); const webpack = require('webpack'); const config = require('./config'); module.exports = { entry: { app: './src/index.tsx', }, output: { filename: 'js/[name].bundle.js', path: config.assetsRoot, publicPath: config.publicPath }, module: { rules: [ { oneOf: [] } ] }, resolve: { extensions: ['.js', '.json', '.jsx', '.ts', '.tsx'] // 自动判断后缀名,引入时可以不带后缀 }, plugins: [] };
接下来我们需要让webpack支持typescript,并且将代码转换为es5,这样才能在低版本的浏览器上运行。
依然是先安装工具:
$ npm i -D babel-loader @babel/core @babel/preset-env @babel/preset-react @babel/polyfill $ npm i core-js@2 # babel的按需引入依赖 $ npm i -D @babel/plugin-proposal-class-properties # 能够在class中自动绑定this的指向 $ npm i -D typescript awesome-typescript-loader # 处理ts,主要就靠它 $ npm i -D html-loader html-webpack-plugin # 顺便把html的支持做好
用了ts,就要有一个tsconfig配置,在根目录新建 tsconfig.json:
{ "compilerOptions": { "target": "es5", "lib": [ "dom", "dom.iterable", "esnext" ], "typeRoots": [ "src/types" // 指定 d.ts 文件的位置,根据具体情况修改 ], "allowJs": true, "skipLibCheck": true, "esModuleInterop": true, "experimentalDecorators": true, "allowSyntheticDefaultImports": true, "strict": true, "forceConsistentCasingInFileNames": true, "module": "esnext", "moduleResolution": "node", "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, "jsx": "react", "baseUrl": ".", }, "include": [ "src" ], "exclude": [ "./node_modules" ] }
来配一下webpack:
// webpack.base.js const APP_PATH = path.resolve(__dirname, '../src'); const HtmlWebpackPlugin = require('html-webpack-plugin'); module: { rules: [ { oneOf: [ { test: /\.(html)$/, loader: 'html-loader' }, { test: /\.(j|t)sx?$/, include: APP_PATH, use: [ { loader: 'babel-loader', options: { presets: [ '@babel/preset-react', // jsx支持 ['@babel/preset-env', { useBuiltIns: 'usage', corejs: 2 }] // 按需使用polyfill ], plugins: [ ['@babel/plugin-proposal-class-properties', { 'loose': true }] // class中的箭头函数中的this指向组件 ], cacheDirectory: true // 加快编译速度 } }, { loader: 'awesome-typescript-loader' } ] }, ] } ] }, plugins: [ new HtmlWebpackPlugin({ inject: true, template: config.indexPath, showErrors: true }), ], optimization: {}
为了以后开发时引入路径方便,我们加个路径别名的配置,需要改webpack配置和tsconfig两处:
webpack
tsconfig
// webpack.base.js resolve: { extensions: ['.js', '.json', '.jsx', '.ts', '.tsx'], alias: { '@': path.resolve(__dirname, '../src/') // 以 @ 表示src目录 } }, { "compilerOptions": { // ... "paths": { "@/*": ["src/*"] } // ... } }
至此,我们完成了最最基本的webpack配置,但暂时还不能打包。
有了 webpack的基础配置,还不足以支持打生产环境能够使用的包,我们还需要增加一些配置。首先,每次打包前最好能把上一次生成的文件删除,这里可以用clean-webpack-plugin插件实现:
clean-webpack-plugin
$ npm i -D clean-webpack-plugin 然后修改webpack基础配置:
$ npm i -D clean-webpack-plugin
// webpack.base.js const { CleanWebpackPlugin } = require('clean-webpack-plugin'); module.exports = { plugins: [ new CleanWebpackPlugin(), ] }
在生产环境,我们希望部署新版本后能够丢弃缓存,又希望保留没有被改动的文件的缓存,而在开发环境,我们希望完全不使用缓存,因此我们需要在当前配置的基础上,分别扩展生产和开发两套配置。
// webpack.prod.js 生产环境打包配置 const merge = require('webpack-merge'); const baseWebpackConfig = require('./webpack.base'); const { CleanWebpackPlugin } = require('clean-webpack-plugin'); module.exports = merge.smart(baseWebpackConfig, { mode: 'production', devtool: sourceMapsMode, output: { filename: 'js/[name].[contenthash:8].js', // contenthash:只有模块的内容改变,才会改变hash值 }, plugins: [ new CleanWebpackPlugin(), ] } // webpack.dev.js 开发环境的配置 const merge = require('webpack-merge'); const baseWebpackConfig = require('./webpack.base'); const config = require('./config'); module.exports = merge.smart(baseWebpackConfig, { mode: 'development', output: { filename: 'js/[name].[hash:8].js', publicPath: config.publicPath // 这里可以省略 }, module: { rules: [ { oneOf: [] } ] }, }
接下来我们编辑build.js,让打包程序真正能够运行起来:
// config/build.js const webpack = require('webpack'); const webpackConfig = require('./webpack.prod'); webpack(webpackConfig, function (err, stats) {});
安装工具并添加启动命令:
$ npm i -D cross-env
// package.json { "scripts": { "dev": "cross-env NODE_ENV=development webpack-dev-server --config ./config/webpack.dev.js", "build": "cross-env NODE_ENV=production node config/build.js" } }
然后运行打包命令,就能看到新生成的dist目录中有已经打包好的文件了:
$ npm run build 打包分析工具 包是打出来了,但是打包好的文件构成是什么样呢,有没有按照我们的需要正确打包呢,我们需要一个分析工具来帮助判断,这就是webpack-bundle-analyzer。
$ npm run build
webpack-bundle-analyzer
$ npm i -D webpack-bundle-analyzer 我们希望根据打包的命令参数,在打包时自动生成或不生成分析报告。
$ npm i -D webpack-bundle-analyzer
// webpack.base.js const argv = require('yargs').argv; const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; const merge = require('webpack-merge'); const bundleAnalyzerReport = argv.report; // 根据命令参数是否含有 'report' 来决定是否生成报告 // 这个配置将合并到最后的配置中 const webpackConfig = { plugins: [] }; if (bundleAnalyzerReport) { webpackConfig.plugins.push(new BundleAnalyzerPlugin({ analyzerMode: 'static', openAnalyzer: false, reportFilename: path.join(config.assetsRoot, './report.html') })); } // 改用merge来合并配置 module.exports = merge(webpackConfig, { // ...configs });
在package.json打包命令中增加参数:
package.json
"scripts": { "build": "cross-env NODE_ENV=production node config/build.js --report" },
运行npm run build,生成的dist目录中会有一个report.html文件,就是我们的分析报告。
npm run build
dist
report.html
现在我们使脚手架支持css,less和css modules:
css
less
css modules
先装工具:$ npm i -D style-loader css-loader less less-loader 增加配置:
$ npm i -D style-loader css-loader less less-loader
// webpack.base.js module: { rules: [ { oneOf: [ // ... configs { test: /\.(less|css)$/, use: [ { loader: 'style-loader' }, { loader: 'css-loader', options: { modules: false // 如果要启用css modules,改为true即可 } }, { loader: 'less-loader', options: { javascriptEnabled: true } } ] }, ] } ] }
我们发现打包好的文件中并没有css,但是css却可以正常工作,这是因为webpack并没有把样式从js中剥离出来。为了方便管理静态资源,充分利用缓存,我们需要将css单独打包。
先安装工具:$ npm i -D optimize-css-assets-webpack-plugin 增加打包配置:
$ npm i -D optimize-css-assets-webpack-plugin
// webpack.prod.js const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin'); // ...webpack configs optimization: { minimizer: [ new OptimizeCSSAssetsPlugin({ cssProcessorOptions: true ? { map: { inline: false }} : {} }) ] }
运行打包命令,就能看到生成的css文件。
使用postcss,可以自动为css增加浏览器前缀。
postcss
安装依赖:$ npm i -D postcss-loader autoprefixer 增加webpack配置:
$ npm i -D postcss-loader autoprefixer
// webpack.base.js,webpack.prod.js { test: /\.(less|css)$/, use: [ { loader: 'style-loader' }, { loader: 'css-loader', options: { modules: false } }, 'postcss-loader', // 注意插入的位置,webpack.prod.js也要加这一项!!! { loader: 'less-loader', options: { javascriptEnabled: true } } ] },
在根目录新建postcss.config.js:
postcss.config.js
module.exports = { plugins: { autoprefixer: {} } };
在package.json中增加配置:
"browserslist": [ "> 1%", "last 2 versions", "not ie <= 8", "iOS >= 8", "Firefox >= 20", "Android > 4.4" ]
这里提供一个利用postcss做基于vh,vw布局的配置例子。
vh
vw
安装依赖:
$ npm i -D postcss-aspect-ratio-mini postcss-px-to-viewport postcss-write-svg $ npm i -D cssnano cssnano-preset-advanced 修改postcss.config.js:
$ npm i -D postcss-aspect-ratio-mini postcss-px-to-viewport postcss-write-svg
$ npm i -D cssnano cssnano-preset-advanced
module.exports = { plugins: { 'postcss-aspect-ratio-mini': {}, // 处理元素容器的宽高比 'postcss-write-svg': { //处理1px边框 utf8: false }, 'postcss-px-to-viewport': { viewportWidth: 750, viewportHeight: 1334, unitPrecision: 3, viewportUnit: 'vw', selectorBlackList: ['.ignore', '.hairlines'], minPixelValue: 1, mediaQuery: false }, cssnano: { 'cssnano-preset-advanced': { zindex: false, // 这里一定要关掉,否则所有的z-index会被设为1 } }, autoprefixer: {} } };
配置完成后,如果是基于750px宽度设计图,那么设计图上1px就直接在样式中写1px即可,打包时会自动转为vw单位。
js的打包基本处理完了,还有图片、音频等静态资源需要处理。
依然先装依赖:
$ npm i -D url-loader file-loader $ npm i -D @svgr/webpack # 顺带支持一下导入svg图片 增加webpack配置:
$ npm i -D url-loader file-loader
$ npm i -D @svgr/webpack # 顺带支持一下导入svg图片
// webpack.base.js { test: /\.svg$/, use: ['@svgr/webpack'] }, { test: /\.(jpg|jpeg|bmp|png|webp|gif)$/, loader: 'url-loader', options: { limit: 8 * 1024, // 小于这个大小的图片,会自动base64编码后插入到代码中 name: 'img/[name].[hash:8].[ext]', outputPath: config.assetsDirectory, publicPath: config.assetsRoot } }, // 下面这个配置必须放在最后 { exclude: [/\.(js|mjs|ts|tsx|less|css|jsx)$/, /\.html$/, /\.json$/], loader: 'file-loader', options: { name: 'media/[path][name].[hash:8].[ext]', outputPath: config.assetsDirectory, publicPath: config.assetsRoot } }
tips: 生产环境需要合理使用缓存,需要拷贝一份同样的配置在webpack.prod.js中,并将name中的hash改为contenthash 接下来我们要把public目录里除了index.html以外的文件都拷贝一份到打包目录中:
安装依赖:$ npm i -D copy-webpack-plugin 增加配置:
$ npm i -D copy-webpack-plugin
// webpack.base.js const CopyWebpackPlugin = require('copy-webpack-plugin'); plugins: [ // ...other plugins new CopyWebpackPlugin([ { from: 'public', ignore: ['index.html'] } ]) ]
有些模块是公共的,如果不把他拆分出来,那么他会在每一个被引入的模块中出现,我们需要优化与此相关的配置。
// webpack.prod.js entry: { app: './src/index.tsx', vendor: ['react', 'react-dom'] // 不变的代码分包 }, optimization: { splitChunks: { chunks: 'all', minChunks: 2, maxInitialRequests: 5, cacheGroups: { // 提取公共模块 commons: { chunks: 'all', test: /[\\/]node_modules[\\/]/, minChunks: 2, maxInitialRequests: 5, minSize: 0, name: 'common' } } } }
通过使用打包分析工具,我们会发现打出来的包都很大,远不能满足生产环境的体积要求,因此还需要对代码进行压缩。
安装依赖:$ npm i -D uglifyjs-webpack-plugin mini-css-extract-plugin compression-webpack-plugin 增加和修改配置:
$ npm i -D uglifyjs-webpack-plugin mini-css-extract-plugin compression-webpack-plugin
// webpack.prod.js const UglifyjsWebpackPlugin = require('uglifyjs-webpack-plugin'); const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const CompressionWebpackPlugin = require('compression-webpack-plugin'); { test: /\.(less|css)$/, use: [ MiniCssExtractPlugin.loader, // 注意书写的顺序 { loader: 'css-loader', }, 'postcss-loader', { loader: 'less-loader', options: { javascriptEnabled: true, } } ] }, // ...configs plugins: [ new HtmlWebpackPlugin({ template: config.indexPath, minify: { removeComments: true, collapseWhitespace: true, removeRedundantAttributes: true, useShortDoctype: true, removeOptionalTags: false, removeEmptyAttributes: true, removeStyleLinkTypeAttributes: true, removeScriptTypeAttributes: true, removeStyleLinkTypeAttributes: true, removeAttributeQuotes: true, removeCommentsFromCDATA: true, keepClosingSlash: true, minifyJS: true, minifyCSS: true, minifyURLs: true, } }), new MiniCssExtractPlugin({ filename: 'css/[name].[contenthash:8].css' // chunkFilename: '[name].[contenthash:8].chunk.css' }), // gzip压缩 new CompressionWebpackPlugin({ filename: '[path].gz[query]', algorithm: 'gzip', test: new RegExp('\\.(' + productionGzipExtensions.join('|') + ')$'), threshold: 10240, // 大于这个大小的文件才会被压缩 minRatio: 0.8 }), ], optimization: { minimizer: [ new UglifyjsWebpackPlugin({ sourceMap: config.productionJsSourceMap }) ] }
运行打包命令,查看打包好的文件,可以看到代码都被压缩好了。
使用terser 由于uglify-es已经停止维护,所以改用目前比较流行的terser来压缩js代码。我们仅需做几处简单的修改。
uglify-es
terser
首先安装依赖:$ npm i -D terser-webpack-plugin 然后改写webpack.prod.js即可:
$ npm i -D terser-webpack-plugin
// const UglifyjsWebpackPlugin = require('uglifyjs-webpack-plugin'); const TerserPlugin = require('terser-webpack-plugin'); optimization: { minimizer: [ // new UglifyjsWebpackPlugin({ // sourceMap: config.productionJsSourceMap // }) new TerserPlugin({ sourceMap: config.productionJsSourceMap }) ] }
打包用的配置基本完成了,现在我们来配置一下开发环境。首先处理通用配置config.js:
module.exports = { // ...configs devServer: { port: 8080, host: 'localhost', contentBase: path.join(__dirname, '../public'), watchContentBase: true, publicPath: '/', compress: true, historyApiFallback: true, hot: true, clientLogLevel: 'error', open: true, overlay: false, quiet: false, noInfo: false, watchOptions: { ignored: /node_modules/ }, proxy: {} } };
然后增加开发配置:
// webpack.dev.js const path = require('path'); const merge = require('webpack-merge'); const baseWebpackConfig = require('./webpack.base'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const webpack = require('webpack'); const config = require('./config'); module.exports = merge.smart(baseWebpackConfig, { mode: 'development', output: { filename: 'js/[name].[hash:8].js', publicPath: config.publicPath }, module: { rules: [ { oneOf: [] } ] }, plugins: [ new HtmlWebpackPlugin({ template: config.indexPath, minify: { html5: true }, hash: false }), new webpack.HotModuleReplacementPlugin() ], devServer: { ...config.devServer } });
在package.json中增加开发环境运行命令:
"scripts": { "dev": "cross-env NODE_ENV=development webpack-dev-server --config ./config/webpack.dev.js" }
运行npm run dev看看效果吧。
npm run dev
自动寻找空闲端口监听 按照上述配置,如果8080端口已经被占用,则webpack开发服务器会报错退出,无法启动,我们可以利用portfinder来自动搜索空闲的端口。
portfinder
首先安装依赖:$ npm i -D portfinder 然后增加如下配置:
$ npm i -D portfinder
// webpack.dev.js const portfinder = require('portfinder'); // 增加依赖 /* 将module.exports = merge.smart()修改为如下形式 */ const devWebpackConfig = merge.smart(/* ... */); /* 寻找可用端口,并返回一个promise类型的配置,webpack可以接收promise作为配置 */ module.exports = new Promise((resolve, reject) => { portfinder.basePort = config.devServer.port; portfinder.getPort((err, port) => { if (err) reject(err) else { devWebpackConfig.devServer.port = port; } resolve(devWebpackConfig) }) });
不要关闭原有的server,再次运行npm run dev看看效果吧。
一般来说,我们在开发应用的时候会面临多个环境差异的问题,例如,我们有:
一个开发环境,提交代码即可立刻看到效果,它的接口地址可能是http://dev-api.tianzhen.tech 一个测试环境,它需要保持一定程度的稳定性,每隔一小时发布一次新版本,接口地址可能是:https://t1-api.tianzhen.tech 预发布环境,它与生产环境共享持久化数据,在这个环境做最后一次检查,等待发布 生产环境,他需要保持高度稳定,一周发布一个版本,接口地址可能是:https://api-tianzhen.tech 四套环境,不同的接口地址,不同的访问地址,可能还涉及到不同的微信、支付宝鉴权。
许多人采用的方案是这样的,写几个不同的配置文件,切换环境时修改引入的配置,但是这样做经常会忘记切环境导致生产事故。这里提供一套自动多环境的配置方案。
依然先安装依赖:
$ npm i -D dotenv dotenv-expand # 从配置文件中读取并注入环境变量 $ npm i -D interpolate-html-plugin # 向模板注入环境变量 在根目录下新建几个环境配置文件:.env,.env.dev,.env.prod,文件名的格式是固定的,符合.env[.name][.local]即可,同名的配置会按照优先级覆盖或自动合并,例如环境名称是dev,那么优先级就是.env.dev.local,.env.dev,.env.local,.env,高优先级覆盖低优先级。
$ npm i -D dotenv dotenv-expand # 从配置文件中读取并注入环境变量
$ npm i -D interpolate-html-plugin # 向模板注入环境变量
.env
.env.dev
.env.prod
.env[.name][.local]
.env.dev.local
.env.local
我们随意编写一个环境变量配置:
// .env.dev // 变量名要以 REACT_APP_ 开头 REACT_APP_ENV='dev' REACT_APP_API_ROOT='http://dev-api.tianzhen.tech'
在config目录下新建一个env.js文件,用这个脚本来读取环境变量配置,用于以后注入到react项目中:
const fs = require('fs'); const path = require('path'); const argv = require('yargs').argv; const dotenv = require('dotenv'); const dotenvExpand = require('dotenv-expand'); const env = argv.env || 'production'; const ENV_FILE_PATH = path.resolve(__dirname, '../.env'); let dotenvFiles = [ `${ENV_FILE_PATH}.${env}.local`, `${ENV_FILE_PATH}.${env}`, env !== 'test' && `${ENV_FILE_PATH}.local`, ENV_FILE_PATH ].filter(Boolean); dotenvFiles.forEach((dotenvFile) => { if (fs.existsSync(dotenvFile)) { dotenvExpand(dotenv.config({ path: dotenvFile })); } }); const REACT_APP = /^REACT_APP_/i; function getClientEnvironment(publicUrl) { publicUrl = process.env.NODE_ENV === 'production' ? publicUrl.slice(0, -1) : ''; const raw = Object.keys(process.env) .filter(key => REACT_APP.test(key)) .reduce( (env, key) => { env[key] = process.env[key]; return env; }, { NODE_ENV: process.env.NODE_ENV || 'production', // webpack在production模式下会自动启用一些配置 APP_ENV: env, PUBLIC_URL: publicUrl } ); const stringified = {}; Object.keys(raw).forEach((key, index) => { stringified['process.env.' + key] = JSON.stringify(raw[key]); }); return { raw, stringified }; } module.exports = getClientEnvironment;
修改webpack配置,向react应用和index.html注入环境变量
react
index.html
// webpack.base.js const InterpolateHtmlPlugin = require('interpolate-html-plugin'); const getClientEnvironment = require('./env'); const env = getClientEnvironment(config.publicPath); plugins: [ new HtmlWebpackPlugin(), // 注意:注入插件一定要在HtmlWebpackPlugin之后使用 // 在html模板中能够使用环境变量 // <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico"> new InterpolateHtmlPlugin(env.raw), // 在js代码中能够使用环境变量(demo: process.env.REACT_APP_ENV === 'dev') new webpack.DefinePlugin(env.stringified), ]
配置都做好了,如何让打包命令知道当前用的哪个环境呢,我们修改一下打包命令,加上env参数:
env
"scripts": { "dev": "cross-env NODE_ENV=development webpack-dev-server --config ./config/webpack.dev.js --env=dev", "build:prod": "cross-env NODE_ENV=production node config/build.js --env=prod --report", "build:t1": "cross-env NODE_ENV=production node config/build.js --env=t1 --report", "build:dev": "cross-env NODE_ENV=production node config/build.js --env=dev --report" }
把同样的配置,分别配置到webpack.prod.js和webpack.dev.js中,然后运行对应打包命令,就可以看到项目中成功注入了环境变量。例如,想要使用.env.dev中的变量,则打包命令中增加参数--env=dev即可,配置将由.env.dev.local,.env.dev,.env.local,.env合并覆盖生成。
--env=dev
webpack根据NODE_ENV的值来自动选择production或development模式编译,因此,如果没有必须要求,尽量不要以NODE_ENV的值做为打包环境依据,否则就要自行处理更复杂的webpack配置。
NODE_ENV
production
development
preload和prefetch是一组能够预读资源,优化用户体验的工具,这里给出一个在首页预读字体和图片的例子,来演示它们结合webpack的使用方法,详见文档。
preload
prefetch
安装依赖:$ npm i -D preload-webpack-plugin 修改webpack.prod.js:
$ npm i -D preload-webpack-plugin
const PreloadWebpackPlugin = require('preload-webpack-plugin') plugins: [ new PreloadWebpackPlugin({ rel: 'preload', as(entry) { if (/\.css$/.test(entry)) return 'style'; if (/\.woff$/.test(entry)) return 'font'; if (/\.png$/.test(entry)) return 'image'; return 'script'; }, include: ['app'] // include:'allChunks' }), ]
配置按需加载,可以将每个页面或组件拆成独立的包,减小首页加载内容的体积,是很好的优化策略。
安装依赖:npm i -D @babel/plugin-syntax-dynamic-import 修改webpack.base.js
npm i -D @babel/plugin-syntax-dynamic-import
{ test: /\.(j|t)sx?$/, include: APP_PATH, use: [ { loader: 'babel-loader', options: { plugins: [ '@babel/plugin-syntax-dynamic-import', // 这是新加入的项 ['@babel/plugin-proposal-class-properties', { 'loose': true }] ], cacheDirectory: true } } ] }
配置完后,就可以用import的方式载入组件了: const HelloWorldPage = import('@/pages/demo/HelloWorldDemo/HelloWorldDemoPage'); 至此,脚手架已经基本可以使用,并且完成了一部分优化。接下来的文章内容主要是围绕开发体验和团队规范展开的,还会涉及到一个比较优秀的react路由实践。
import
const HelloWorldPage = import('@/pages/demo/HelloWorldDemo/HelloWorldDemoPage');
webpack的基本配置
安装webpack相关工具:
$ npm i -D webpack webpack-cli webpack-dev-server webpack-merge
在config
目录下新建几个文件:config.js
,webpack.base.js
,webpack.prod.js
,webpack.dev.js
,build.js
先抽取一些通用的配置:
babel和typescript,路径别名
接下来我们需要让webpack支持typescript,并且将代码转换为es5,这样才能在低版本的浏览器上运行。
依然是先安装工具:
用了ts,就要有一个tsconfig配置,在根目录新建 tsconfig.json:
来配一下webpack:
为了以后开发时引入路径方便,我们加个路径别名的配置,需要改
webpack
配置和tsconfig
两处:至此,我们完成了最最基本的webpack配置,但暂时还不能打包。
完善 webpack 打包配置
有了
webpack
的基础配置,还不足以支持打生产环境能够使用的包,我们还需要增加一些配置。首先,每次打包前最好能把上一次生成的文件删除,这里可以用clean-webpack-plugin
插件实现:$ npm i -D clean-webpack-plugin
然后修改webpack基础配置:在生产环境,我们希望部署新版本后能够丢弃缓存,又希望保留没有被改动的文件的缓存,而在开发环境,我们希望完全不使用缓存,因此我们需要在当前配置的基础上,分别扩展生产和开发两套配置。
接下来我们编辑build.js,让打包程序真正能够运行起来:
安装工具并添加启动命令:
$ npm i -D cross-env
然后运行打包命令,就能看到新生成的dist目录中有已经打包好的文件了:
$ npm run build
打包分析工具 包是打出来了,但是打包好的文件构成是什么样呢,有没有按照我们的需要正确打包呢,我们需要一个分析工具来帮助判断,这就是webpack-bundle-analyzer
。$ npm i -D webpack-bundle-analyzer
我们希望根据打包的命令参数,在打包时自动生成或不生成分析报告。在
package.json
打包命令中增加参数:运行
npm run build
,生成的dist
目录中会有一个report.html
文件,就是我们的分析报告。支持less和css modules
现在我们使脚手架支持
css
,less
和css modules
:先装工具:
$ npm i -D style-loader css-loader less less-loader
增加配置:提取css
我们发现打包好的文件中并没有css,但是css却可以正常工作,这是因为webpack并没有把样式从js中剥离出来。为了方便管理静态资源,充分利用缓存,我们需要将css单独打包。
先安装工具:
$ npm i -D optimize-css-assets-webpack-plugin
增加打包配置:运行打包命令,就能看到生成的css文件。
自动增加css前缀
使用
postcss
,可以自动为css
增加浏览器前缀。安装依赖:
$ npm i -D postcss-loader autoprefixer
增加webpack配置:在根目录新建
postcss.config.js
:在package.json中增加配置:
postcss-px-to-viewport示例
这里提供一个利用
postcss
做基于vh
,vw
布局的配置例子。安装依赖:
$ npm i -D postcss-aspect-ratio-mini postcss-px-to-viewport postcss-write-svg
$ npm i -D cssnano cssnano-preset-advanced
修改postcss.config.js
:配置完成后,如果是基于750px宽度设计图,那么设计图上1px就直接在样式中写1px即可,打包时会自动转为
vw
单位。处理静态资源
js的打包基本处理完了,还有图片、音频等静态资源需要处理。
依然先装依赖:
$ npm i -D url-loader file-loader
$ npm i -D @svgr/webpack # 顺带支持一下导入svg图片
增加webpack配置:tips: 生产环境需要合理使用缓存,需要拷贝一份同样的配置在webpack.prod.js中,并将name中的hash改为contenthash 接下来我们要把public目录里除了index.html以外的文件都拷贝一份到打包目录中:
安装依赖:
$ npm i -D copy-webpack-plugin
增加配置:提取公共模块,拆分代码
有些模块是公共的,如果不把他拆分出来,那么他会在每一个被引入的模块中出现,我们需要优化与此相关的配置。
压缩代码(2019/9/18更新)
通过使用打包分析工具,我们会发现打出来的包都很大,远不能满足生产环境的体积要求,因此还需要对代码进行压缩。
安装依赖:
$ npm i -D uglifyjs-webpack-plugin mini-css-extract-plugin compression-webpack-plugin
增加和修改配置:运行打包命令,查看打包好的文件,可以看到代码都被压缩好了。
使用terser 由于
uglify-es
已经停止维护,所以改用目前比较流行的terser
来压缩js代码。我们仅需做几处简单的修改。首先安装依赖:
$ npm i -D terser-webpack-plugin
然后改写webpack.prod.js
即可:配置webpack开发服务器
打包用的配置基本完成了,现在我们来配置一下开发环境。首先处理通用配置config.js:
然后增加开发配置:
在
package.json
中增加开发环境运行命令:运行
npm run dev
看看效果吧。自动寻找空闲端口监听 按照上述配置,如果8080端口已经被占用,则
webpack
开发服务器会报错退出,无法启动,我们可以利用portfinder
来自动搜索空闲的端口。首先安装依赖:
$ npm i -D portfinder
然后增加如下配置:不要关闭原有的server,再次运行
npm run dev
看看效果吧。自定义多环境
一般来说,我们在开发应用的时候会面临多个环境差异的问题,例如,我们有:
一个开发环境,提交代码即可立刻看到效果,它的接口地址可能是http://dev-api.tianzhen.tech 一个测试环境,它需要保持一定程度的稳定性,每隔一小时发布一次新版本,接口地址可能是:https://t1-api.tianzhen.tech 预发布环境,它与生产环境共享持久化数据,在这个环境做最后一次检查,等待发布 生产环境,他需要保持高度稳定,一周发布一个版本,接口地址可能是:https://api-tianzhen.tech 四套环境,不同的接口地址,不同的访问地址,可能还涉及到不同的微信、支付宝鉴权。
许多人采用的方案是这样的,写几个不同的配置文件,切换环境时修改引入的配置,但是这样做经常会忘记切环境导致生产事故。这里提供一套自动多环境的配置方案。
依然先安装依赖:
$ npm i -D dotenv dotenv-expand # 从配置文件中读取并注入环境变量
$ npm i -D interpolate-html-plugin # 向模板注入环境变量
在根目录下新建几个环境配置文件:.env
,.env.dev
,.env.prod
,文件名的格式是固定的,符合.env[.name][.local]
即可,同名的配置会按照优先级覆盖或自动合并,例如环境名称是dev,那么优先级就是.env.dev.local
,.env.dev
,.env.local
,.env
,高优先级覆盖低优先级。我们随意编写一个环境变量配置:
在
config
目录下新建一个env.js文件,用这个脚本来读取环境变量配置,用于以后注入到react项目中:修改
webpack
配置,向react
应用和index.html
注入环境变量配置都做好了,如何让打包命令知道当前用的哪个环境呢,我们修改一下打包命令,加上
env
参数:把同样的配置,分别配置到
webpack.prod.js
和webpack.dev.js
中,然后运行对应打包命令,就可以看到项目中成功注入了环境变量。例如,想要使用.env.dev
中的变量,则打包命令中增加参数--env=dev
即可,配置将由.env.dev.local
,.env.dev
,.env.local
,.env
合并覆盖生成。webpack
根据NODE_ENV
的值来自动选择production
或development
模式编译,因此,如果没有必须要求,尽量不要以NODE_ENV
的值做为打包环境依据,否则就要自行处理更复杂的webpack
配置。preload,prefetch
preload
和prefetch
是一组能够预读资源,优化用户体验的工具,这里给出一个在首页预读字体和图片的例子,来演示它们结合webpack
的使用方法,详见文档。安装依赖:
$ npm i -D preload-webpack-plugin
修改webpack.prod.js
:配置按需加载
配置按需加载,可以将每个页面或组件拆成独立的包,减小首页加载内容的体积,是很好的优化策略。
安装依赖:
npm i -D @babel/plugin-syntax-dynamic-import
修改webpack.base.js
配置完后,就可以用
import
的方式载入组件了:const HelloWorldPage = import('@/pages/demo/HelloWorldDemo/HelloWorldDemoPage');
至此,脚手架已经基本可以使用,并且完成了一部分优化。接下来的文章内容主要是围绕开发体验和团队规范展开的,还会涉及到一个比较优秀的react路由实践。