React - Webpack 开发环境重新搭建
React - Webpack 开发环境重新搭建说明距离初次使用 react 已经有一年半左右的时间,无论是 react 全家桶(redux react-router 等)还是构建工具 webpack 等都有了版本的更新,配置使用上多少都发生了变化,这里针对当前最新的版本重新搭建做一下记录方便日后查询。webpack 环境配置参考了 vue-cli 中的 webpack
React - Webpack 开发环境重新搭建
说明
距离初次使用 react 已经有一年半左右的时间,无论是 react 全家桶(redux react-router 等)还是构建工具 webpack 等都有了版本的更新,配置使用上多少都发生了变化,这里针对当前最新的版本重新搭建做一下记录方便日后查询。
webpack
环境配置参考了vue-cli
中的webpack
配置,开发服务器没有用express
而是webpack-dev-server
重要依赖版本
node: v8.9.0 | npm: v5.6.0 | yarn: v1.3.2 |
---|
- react: 16.2.0
- redux: 3.7.2
- react-router-dom: 4.2.2
- react-redux: 5.0.6
- react-router-redux: 5.0.0-alpha.9
- axios: 0.17.1
- react-hot-loader: 4.0.0-beta.12
- webpack: 3.10.0
- webpack-dev-server: 2.9.7
- babel-core: 6.26.0
- babel-preset-env: 1.6.1
- babel-plugin-transform-runtime: 6.23.0
目录结构调整
1. 外层目录、文件说明
├── build // webpack 构建工具配置文件夹
├── config // 用户自定义选项
├── dist // 生产编译后的文件夹,需要提交的文件
├── server // 开发时,本地服务器
├── src // 源码目录
├── .babelrc // babel 配置文件
├── .eslintignore // eslint 规则忽略文件
├── .eslintrc.js // eslint 规则配置文件
├── .gitignore // git 提交忽略文件
├── .postcssrc.js // postcss 配置文件
├── package.json
└── yarn.lock
2. 构建工具配置文件/build | /config
说明
├── build
│ ├── build.js // 生产环境启动文件 yarn run build
│ ├── dev-server.js // 开发环境启动文件 yarn start
│ ├── utils.js // 一些工具函数
│ ├── webpack.base.config.js // 基础公用 webpack 配置
│ ├── webpack.dev.config.js // 开发环境配置
│ └── webpack.prod.config.js // 生产环境配置
├── config
│ └── index.js // 从 webpack 中抽取的可配置的选项
3. 源码目录/src
文件说明
├── api
│ ├── request.js // 网络请求的封装文件
│ └── home.js // 抽取某个页面的网络请求部分
├── assets // 资源文件夹 图片、音频、视频等
├── components // 展示型组件,按照容器组件分文件夹
│ ├── App // App 容器组件的展示型子组件目录
│ └── index.js // 顶层zi'zu'jian
├── containers // 容器组件
│ └── App.js
├── modules // redux 模块,包括 reducer 部分与 action 部分
│ ├── home // 对应某个容器组件,集中了这个容器的 reducer 和 action
│ │ ├── actions.js
│ │ └── reducer.js
│ ├── reducers.js // 合并后的总的 reducer
│ └── types-constant.js // 抽取出的 type 常量
├── scss // 样式文件
│ ├── _common.scss
│ └── home.scss
├── store // store 配置
│ ├── configureStore.js
│ └── index.js
└── utils // 公用的工具函数
│ └── bindActions.js
├── index.html // 页面模板
├── routes.js // 路由配置
└── index.js // react 入口
react-webpack 开发环境搭建
命令
"scripts": {
"dev": "node build/dev-server.js",
"start": "npm run dev",
"build": "node build/build.js",
"server": "node server/index.js"
},
yarn run dev # yarn start
开发环境启动yarn run build
生产环境启动yarn run build --report
显示打包详情
1. 创建 /config/index.js
配置文件
用来定义一些在 webpack 环境搭建中的可配置项,主要分为三个部分,
common
通用配置development
开发环境的配置production
生产环境的配置
const ip = require('ip').address(); // ip 模块用来获取本机 ip
const utils = require('../build/utils'); // 自定义的一组工具函数
const resolve = utils.resolve(__dirname, '../'); // utils.resolve() 函数处理路径拼接
module.exports = {
common: {
context: resolve(''), // 定义根目录路径
sourceCode: resolve('src'), // 源码目录路径
// 封装的请求模块位置,用于注入请求与服务器地址
requestModule: resolve('src/api/request.js')
},
development: {
env: {NODE_ENV: JSON.stringify('development')}, // 开发环境的环境变量
port: process.env.PORT || 8273, // 设置开发时端口号
devServerIp: ip, // 开发时的 Ip
basicRequestLink: `http://${ip}:3167`, // 注入的请求服务器地址
entryPath: null, // 入口文件路径,默认为 './src/index.js'
assetsRoot: resolve('dev'), // 开发时编译后的文件路径,不会显示
assetsSubDirectory: 'static', // 二级资源路径
assetsPublicPath: '/', // 编译发布的根目录
},
production: {
env: {NODE_ENV: JSON.stringify('production')}, // 生产环境的环境变量
basicRequestLink: `https://xxx.com`, // 生产时设置为最后部署的服务器地址
entryPath: null, // 入口文件路径,默认为 './src/index.js'
assetsRoot: resolve('dist'), // 编译后的文件路径
assetsSubDirectory: 'static', // 二级资源路径
assetsPublicPath: '/', // 编译发布的根目录
productionSourceMap: false, // js sourceMap
bundleAnalyzerReport: utils.shouldReport(), // 是否显示 report 页面,也就是各个模块的打包细节
}
}
以上涉及到几个定义在
/build/utils.js
中的函数
1. utils.resolve()
对
path.join()
的封装,保存一个基础的路径,并返回一个函数,可以拼接路径到到基础路径上
exports.resolve = function (...basicPath) {
return function (dir) {
return path.join(...basicPath, dir || '');
}
}
2. utils.shouldReport()
解析命令行参数,判断是否可以进行 report 处理,展示模块打包细节
exports.shouldReport = function () {
if (process.env.npm_config_report) {
return process.env.npm_config_report;
}
return process.argv.some((item) => item === '--report');
}
注意:通过
npm run build --report
执行 会产生process.env.npm_config_report
, 通过yarn run build --report
则可以在process.argv
中拿到配置。
2. 创建 webpack.base.config.js
基础通用 webpack 配置文件
const HtmlWebpackPlugin = require('html-webpack-plugin'); // 根据模板生成 HTML
const packageConfig = require('../package.json');
const config = require('../config/index');
const utils = require('./utils');
const common = config.common;
const current = utils.getEnvAndConf(config); // 得到当前的 NODE_ENV 环境变量和对应的配置
const namedAssets = utils.resolve(current.conf.assetsSubDirectory); // 二级资源路径拼接函数
module.exports = {
context: common.context,
entry: utils.computeEntry(config, packageConfig), // computeEntry() 根据环境返回对应的入口配置
output: utils.computeOutput(config), // computeOutput() 根据环境返回对应的出口配置
cache: true,
resolve: {
extensions: [ // 默认的扩展名,使得引用的时候可以不带相应的扩展名
'.js', '.json', '.jsx', '.css'
],
modules: ['node_modules', common.sourceCode] // 告诉 webpack 解析模块时应该搜索的目录
},
module: { // 配置模块处理
rules: [
{
test: /\.(js|jsx)$/,
loader: 'eslint-loader',
enforce: 'pre', // 预先进行 eslint 语法检查
include: common.sourceCode,
options: {
formatter: require('eslint-friendly-formatter')
}
}, {
test: /\.(js|jsx)$/,
loader: 'babel-loader',
include: common.sourceCode
}, {
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: namedAssets(
current.env !== 'production'
? 'imgs/[name].[ext]'
: 'imgs/[name].[hash:10].[ext]')
}
}, {
test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: namedAssets(
current.env !== 'production'
? 'media/[name].[ext]'
: 'media/[name].[hash:10].[ext]')
}
}, {
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: namedAssets(
current.env !== 'production'
? 'fonts/[name].[ext]'
: 'fonts/[name].[hash:10].[ext]')
}
}, {
test: require.resolve(common.requestModule), // 将配置中的服务器地址注入到指定模块中
loader: 'imports-loader?basicRequestLink=>' + JSON.stringify(current.conf.basicRequestLink)
}
]
},
plugins: [
new HtmlWebpackPlugin({ // html 插件
template: utils.resolve(common.sourceCode)('index.html'),
filename: 'index.html',
inject: 'body',
minify: false,
xhtml: true,
cache: false,
// favicon: ''
})
]
}
基础 webpack 配置中涉及到了几个
/build/utils.js
中的函数
1. utils.getEnvAndConf(config)
获取当前的
env.NODE_ENV
环境以及对应的配置,需要传入参数 config 就是在/config/index.js
中配置的
exports.getEnvAndConf = function (config) {
const env = process.env.NODE_ENV;
const conf = config[env];
return {env, conf};
}
2. utils.computeEntry(config, packageConfig)
根据当前环境,返回相应的入口文件配置,开发环境配置 HMR
exports.computeEntry = function (config, packageConfig) {
// 先解析出,当前的环境,和对应的配置
const {env, conf} = exports.getEnvAndConf(config);
let entry = {};
if(env === 'production') {
// 生产环境
entry.app = conf.entryPath || './src/index.js';
} else if (env === 'development') {
// 开发环境
const {port, devServerIp, entryPath} = conf;
entry.app = [
`webpack-dev-server/client?http://${devServerIp}:${port}`,
'webpack/hot/only-dev-server',
entryPath || './src/index.js'
];
}
// 将项目模块与导入的模块分离,也就是 package.json 中的 dependencies 部分
entry.vendor = Object.keys(packageConfig.dependencies);
return entry;
}
3. utils.computeOutput(config)
根据当前环境,返回相应的出口文件配置,生产环境添加 hash
exports.computeOutput = function (config) {
// 先解析出,当前的环境,和对应的配置
const {env, conf} = exports.getEnvAndConf(config);
const filename = path.join(
conf.assetsSubDirectory,
env !== 'production' ? 'js/[name].bundle.js' : 'js/[name].[chunkhash:10].bundle.js'
);
const chunkFilename = env !== 'production'
? '[id].js'
: '[id].[chunkhash:10].js';
const output = {
path: conf.assetsRoot,
publicPath: conf.assetsPublicPath,
filename,
chunkFilename
};
return output;
}
3. 创建 webpack.dev.config.js
开发环境 webpack 配置
主要是添加 开发环境的 plugin 插件
const webpack = require('webpack');
const merge = require('webpack-merge'); // 用来合并 webpack 配置
const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin'); // 优化错误提示插件
const CleanWebpackPlugin = require('clean-webpack-plugin'); // 构建前清空目录插件
const baseWebpackConfig = require('./webpack.base.config');
const utils = require('./utils');
const config = require('../config/index');
const common = config.common;
const current = utils.getEnvAndConf(config);
module.exports = merge(baseWebpackConfig, {
devtool: '#cheap-module-eval-source-map',
module: {
rules: [
{
test: /\.(scss|sass|css)$/, // 样式文件处理
include: common.sourceCode,
use: utils.computeStyleLoader(false, ['style-loader', 'css-loader', 'postcss-loader', 'sass-loader'])
}
]
},
plugins: [
new CleanWebpackPlugin(['dev'], {root: common.context}),
// 使用模块的路径,而不是数字标识符作为ID,避免解析顺序引起的 hash 变化
new webpack.NamedModulesPlugin(),
// 热模块替换插件
new webpack.HotModuleReplacementPlugin(),
// 定义process.env.NODE_ENV环境变量
new webpack.DefinePlugin({'process.env.NODE_ENV': current.conf.env.NODE_ENV}),
// 将引入的第三方库拆分出来
new webpack.optimize.CommonsChunkPlugin({name: 'vendor'}),
// 将运行时代码拆分出来,配合其他插件避免每次打包 hash 都变化
new webpack.optimize.CommonsChunkPlugin({name: 'runtime'}),
// 编译出现错误时,跳过输出阶段,确保输出资源不会包含错误
new webpack.NoEmitOnErrorsPlugin(),
new FriendlyErrorsPlugin()
]
})
用到了一个
/build/utils.js
中的函数
1. utils.computeStyleLoader()
抽取出的,方便配置 样式的 loader
// 如果有了 less-laoder 页可以在这里配置
exports.styleLoadersOptions = {
dev: {
'sass-loader': {
outputStyle: 'expanded',
sourceMapContents: true,
sourceMap: true
}
},
prod: {
'sass-loader': {outputStyle: 'expanded'}
}
}
exports.computeStyleLoader = function (isProduction, loaders) {
const optionsMap = exports.styleLoadersOptions[isProduction ? 'prod' : 'dev'];
const defaultOptions = isProduction ? {} : {sourceMap: true};
return loaders.map(loader => {
const options = optionsMap[loader] || defaultOptions;
return {loader, options};
})
}
4. 创建 dev-server.js
开发环境启动文件
- 设置环境变量
env.NODE_ENV
- 配置
webpack-dev-server
- 开启一个服务器,打开一个页面
const config = require('../config/index');
const devConfig = config.development;
// 设置环境变量
if (!process.env.NODE_ENV) {
process.env.NODE_ENV = JSON.parse(devConfig.env.NODE_ENV)
}
const webpack = require('webpack');
const WebpackDevServer = require('webpack-dev-server');
const opn = require('opn'); // 一个 node 插件,在浏览器中打开指定链接
const chalk = require('chalk'); // 命令行输出美化插件
const webpackConfig = require('./webpack.dev.config');
const utils = require('./utils');
const common = config.common;
const resolve = utils.resolve(common.context);
// 配置 webpack-dev-server
const devServerOptions = {
contentBase: resolve('dev'),
publicPath: devConfig.assetsPublicPath,
historyApiFallback: true,
clientLogLevel: 'none',
hot: true,
inline: true,
compress: true,
openPage: 'index.html',
stats: {
colors: true,
errors: true,
warnings: true,
modules: false,
chunks: false
}
};
// 开启服务
const compiler = webpack(webpackConfig);
const server = new WebpackDevServer(compiler, devServerOptions);
const {port, devServerIp, basicRequestLink} = devConfig;
server.listen(port, devServerIp, () => {
const link = `http://${devServerIp}:${port}`;
console.log(chalk.cyan(`Starting server on ${link}`));
console.log(chalk.cyan(`development data server on ${basicRequestLink}`));
// 成功后打开指定链接
opn(link).then(() => {
console.log(chalk.cyan('success open ...'));
}).catch(err => {
console.log(chalk.red(err));
})
})
5. 创建 webpack.prod.config.js
生产环境配置
主要配置 plugin 插件
const webpack = require('webpack');
const merge = require('webpack-merge');
const ExtractTextPlugin = require('extract-text-webpack-plugin'); // 分离出样式文件插件
const CleanWebpackPlugin = require('clean-webpack-plugin');
const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin'); // 优化 css 插件
const baseWebpackConfig = require('./webpack.base.config');
const utils = require('./utils');
const config = require('../config/index');
const common = config.common;
const current = utils.getEnvAndConf(config);
let reportPlugin = [];
if (current.conf.bundleAnalyzerReport) {
// 如果可以显示 report 则添加如下插件
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
reportPlugin.push(new BundleAnalyzerPlugin());
}
module.exports = merge(baseWebpackConfig, {
devtool: current.conf.productionSourceMap ? '#source-map' : false,
module: {
rules: [
{
test: /\.(scss|sass|css)$/, // 样式文件 loader 配置,并分离输出
include: common.sourceCode,
use: ExtractTextPlugin.extract({
fallback: 'style-loader',
use: utils.computeStyleLoader(true, ['css-loader', 'postcss-loader', 'sass-loader'])
})
}
]
},
plugins: [
new CleanWebpackPlugin(['dist'], {root: common.context}),
// 根据模块的相对路径生成一个四位数的hash作为模块id, 避免解析顺序引起的 hash 变化
new webpack.HashedModuleIdsPlugin(),
// 作用域提升插件
new webpack.optimize.ModuleConcatenationPlugin(),
// 设置生产环境变量
new webpack.DefinePlugin({'process.env.NODE_ENV': current.conf.env.NODE_ENV}),
// 配合输出 css
new ExtractTextPlugin({
filename: utils.resolve(current.conf.assetsSubDirectory)('css/[name].[contenthash:10].css'),
disable: false,
allChunks: true
}),
// 优化合并输出的css
new OptimizeCSSPlugin({cssProcessorOptions: {safe: true}}),
// 压缩 js
new webpack.optimize.UglifyJsPlugin({
compress: {
warnings: false,
'drop_debugger': true,
'drop_console': true
},
comments: false,
'space_colon': false
}),
// 拆分模块
new webpack.optimize.CommonsChunkPlugin({name: 'vendor'}),
new webpack.optimize.CommonsChunkPlugin({name: 'runtime'}),
...reportPlugin
]
});
6. 创建 build.js
生产环境启动文件
const config = require('../config/index');
const prodConfig = config.production;
// 设置环境变量
process.env.NODE_ENV = JSON.parse(prodConfig.env.NODE_ENV)
const webpack = require('webpack');
const ora = require('ora'); // 一个命令行 loading 插件
const chalk = require('chalk'); // 命令行输出美化
const webpackConfig = require('./webpack.prod.config');
// loading
const spinner = ora('building for production...');
spinner.start();
webpack(webpackConfig, function(err, stats) {
spinner.stop();
if (err) {
throw err
}
process
.stdout
.write(stats.toString({colors: true, modules: false, children: false, chunks: false, chunkModules: false}) + '\n\n')
if (stats.hasErrors()) {
console.log(chalk.red(' Build failed with errors.\n'))
process.exit(1)
}
console.log(chalk.cyan(' Build complete.\n'))
console.log(chalk.yellow(' Tip: built files are meant to be served over an HTTP server.\n' + ' Opening index.html over file:// won\'t work.\n'))
})
一些插件的配置文件
1. babel 的配置文件 .babelrc
babel 相关插件
babel-core
: ^6.26.0babel-eslint
: ^8.1.2babel-loader
: ^7.1.2babel-plugin-transform-decorators-legacy
: ^1.3.4babel-plugin-transform-runtime
: ^6.23.0babel-preset-env
: ^1.6.1babel-preset-react
: ^6.24.1babel-preset-stage-2
: ^6.24.1babel-register
: ^6.26.0
{
"presets": [
["env", {
"modules": false,
"targets": {
"node": "current",
"browsers": ["> 1%", "last 2 versions", "not ie <= 8"]
}
}],
"stage-2",
"react"
],
"plugins": [
"transform-runtime", "transform-decorators-legacy", "react-hot-loader/babel"
]
}
2. eslint 配置文件 .eslintrc.js
详情查看源码地址
3. postcss 配置文件 .postcssrc.js
需要安装插件
sudo yarn add autoprefixer
更多插件查看
module.exports = {
"plugins": {
// to edit target browsers: use "browserslist" field in package.json
"autoprefixer": {}
}
}
4. react-hot-loader@4.0.0-beta.12
使用
与版本 3 的使用有很大区别
1. 在 .babelrc
中配置插件
"plugins": ["react-hot-loader/babel"]
2. webpack 插件
plugins: [
new webpack.HotModuleReplacementPlugin()
]
3. react 入口文件中 /src/index.js
import React from 'react'
import { render } from 'react-dom'
import App from './App' // 引入组件,在这个组件中进行热更新
const root = document.createElement('div')
document.body.appendChild(root)
render(<App />, root)
4. App 组件中
import React from 'react'
import { hot } from 'react-hot-loader'
import Counter from './Counter'
const App = () => (
<h1>
Hello, world.<br />
<Counter />
</h1>
)
// 使用 react-hot-loader 提供的 hot 方法代替以前的 AppContainer 组件,更加方便
export default hot(module)(App)
5. 配合 redux react-router
查询源码
更多推荐
所有评论(0)