导语:学习搭建react +webpack工程时,看了很多资料,学习点真的很多,几乎每一项都可以单独写一个篇幅,该文章只在怎么搭建出开发框架,很多东西没有深入,只做了一些简单的介绍,但基本会把官方文档列出来,想要深入了解的可以先自己把工程跑起来,然后认真看官方文档。
我们想要的框架需要什么样的功能:
1、使用webpack打包
3、入口JS文件可以自动注入到HTML模板中
2、使用ES6 react语法
4、可以按需提取打包公用JS
3、使用SCSS,且想要框架自动对需要加兼容的CSS属性做处理
3、单页应用,需要react+router路由
4、单页应用,需要考虑各页面的JS按需加载
5、组件热更新,加快开发速度
5、生产环境下,希望图片能自动压缩,且小于3K的自动转成baseuri格式,CSS可以单独提取出来,且JS CSS可以压缩
6、可以查看打包信息进行打包模块优化
搭建步骤
1、初始化一个npm或yarn工程
执行以下命令,创建一个webpack-react目录,并初始化
mkdir webpack-react
cd webpack-react
npm init
// 若使用yarn,则执行
yarn init
执行完成后,会在webpack-react目录下生成一个package.json文件,文件中有初始化时你填入的内容
2、使用webpack
- 安装webpack(使用webpack 3)
首先需要在你的工程中安装webpack,执行命令:
npm install webpack --save-dev
// 或
yarn add webpack --dev
注:--save-dev参数会让依赖包添加到package.json文件中的devDependencies中
devDependencies与dependencies的区别是:
在其他工程引入你的包时,添加到dependencies中的依赖,会被自动下载。而devDependencies中的依赖不会,devDependencies表示只在该工程开发环境时需要。
- 配制及运行webpack
webpack有很多的配制参数,我们在运行webpack命令时,webpack会自动去项目根目录寻找webpack.config.js文件,也可指定配制文件,如运行命令
webpack --config mycofing.js
在我们的项目webpack-react目录下创建文件webpack.config.js,并写入以下内容:
module.exports = {
// 入口文件
entry: {
app: './src/index.js'
},
output: {
// chunkhash hash的区别:hash是所有输出文件共用一个hash,chunkhash是不同文件是不同的hash,可以用这个做缓存
// 是入口文件的输出名字
filename: '[name].[hash:4].bundle.js',
// 输出绝对路径
path: path.resolve(__dirname, 'dist'),
}
}
在项目根目录下再创建一个src目录,用来存放我们的JS文件
在src目录下创建一个index.js,里面随便写入一些JS代码,但暂时不要使用ES6语法,因为目前整个框架还没有做对ES6语法支持的配制。
执行命令:
webpack
此时会在你的项目根目录下生成一个dist文件夹,dist文件夹中会生成一个app.js文件
我们只需要在HTML文件中引用app.js文件即可。
3、入口JS文件自动注入到HTML模板中
有时我们生成的JS入口文件的名字是变化的,如上面output参数中配制filename: [name].[hash:4].bundle.js,这样我们每次改动后重新打包,JS文件名都会变化,我们每次都得重新修改HTML。
webpack给我们提供了一个插件来帮助我们解决这个问题:HtmlWebpackPlugin
- 安装HtmlWebpackPlugin
npm install HtmlWebpackPlugin --save-dev
- 在src目录下创建一个index.html,作为html模板
<html>
<head>
<title>webpack配制学习</title>
</head>
<body>
</body>
</html>
- 配制,修改webpack.config.js文件
// 处理HTML,可以将所有的入口文件注册到HTML模板中
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
// 入口文件
entry: {
app: './src/index.js'
},
output: {
// chunkhash hash的区别:hash是所有输出文件共用一个hash,chunkhash是不同文件是不同的hash,可以用这个做缓存
// 是入口文件的输出名字
filename: '[name].[hash:4].bundle.js',
// 输出绝对路径
path: path.resolve(__dirname, 'dist'),
},
plugins: [
new HtmlWebpackPlugin({
title: 'webpack配制学习',
// 指定打包出来的html的名字,默认是在output指定的path路径下创建一个叫index.html文件
// filename: 'test/index.html',
// 指定模板,也可以指定模板的loader,如handlebars来加载解析这种模板,也可以在module.loaders中指定
// template: '!!handlebars!src/index.hbs',
template: 'src/index.html',
// minify: {
// html5: true
// }
// 若为true会在引入的JS后面加上?hash
// hash: true,
// cache: false,
})
]
}
- 运行webpack命令后,你会发现在dist文件夹下会自动生成一个index.html文件,文件内会在body标签中自动注入一个script标签,src指向新生成的bundle.js文件
注:当我们执行了多次命令后,会发现在dist文件下生成了多个文件,为了方便查看我们最新生成的文件,可以在执行webpack命令前执行以下命令:rm -rf dist。我们可以将这些命令写入到package.json 文件的script脚本中,如:
"scripts": {
"start": "rm -rf dist && webpack"
}
这样我们可以直接在命令行中执行:npm start即可
4、使用react、ES6语法
在往下之前,我们再改造一下我们的启动脚本,在本地使用服务器的方式运行我们的页面。修改package.json文件:
"scripts": {
"start": "webpack-dev-server",
"build": "rm -rf dist && webpack"
}
在开发环境使用webpack-dev-server,很方便,我们不用自己去启用一个node服务器。想要了解更多webpack-dev-server的配制,可以参考官方文档https://doc.webpack-china.org/guides/development/#-webpack-dev-server,这里不做过多的说明。
因为ES6及更高的JS语法如类属性等,有些浏览器是不支持的,我们需要使用 babel 做一个转换。这里用到了webpack的loader配制选项,官方推荐的loader列表:https://doc.webpack-china.org/loaders/babel-loader/
- 安装依赖包:
// babel
yarn add babel-loader babel-core --dev
// babel插件
yarn add babel-preset-es2015 babel-preset-react babel-plugin-transform-class-properties --dev
// react,因是项目依赖的包,放入dependencies中
yarn add react react-dom --save
注:babel有很多插件可以安装使用,具体可参考:https://babeljs.io/docs/plugins/
- 配制、修改webpack.config.js文件
// 处理HTML,可以将所有的入口文件注册到HTML模板中
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
// 入口文件
entry: {
app: './src/index.js'
},
output: {
// chunkhash hash的区别:hash是所有输出文件共用一个hash,chunkhash是不同文件是不同的hash,可以用这个做缓存
// 是入口文件的输出名字
filename: '[name].[hash:4].bundle.js',
// 输出绝对路径
path: path.resolve(__dirname, 'dist'),
},
module: {
rules: [
{
// .js 或.jsx格式的文件都会使用下面配制的loader去解析
test: /\.jsx?$/,
exclude: /node_modules/,
use: [
// 使用babel-loader解析
{
loader: 'babel-loader',
options: {
// 支持react ES6语法
presets: ['react', 'es2015'],
plugins: [
// 支持calss属性
'transform-class-properties'
]
}
}
]
}
]
},
plugins: [
new HtmlWebpackPlugin({
title: 'webpack配制学习',
// 指定打包出来的html的名字,默认是在output指定的path路径下创建一个叫index.html文件
// filename: 'test/index.html',
// 指定模板,也可以指定模板的loader,如handlebars来加载解析这种模板,也可以在module.loaders中指定
// template: '!!handlebars!src/index.hbs',
template: 'src/index.html',
// minify: {
// html5: true
// }
// 若为true会在引入的JS后面加上?hash
// hash: true,
// cache: false,
})
]
}
以上配制指明了.js或.jsx文件将会先使用babel-loader进行转换,babel-loader中指明了支持es6 react语法,以及calss的属性
- 在JS中使用react、ES6语法,例:
import React from 'react';
export default class Test extends React.Component {
static propTypes = {
name: React.PropTypes.string,
}
render() {
return (
<div>This is test page!</div>
);
}
}
5、按需提取打包公用JS
当我们项目中模块增多时,很多不同的模块中都引入了相同的模块,比如react包,此时我们需要将一些公用的模块提取出来,单独打包,此时会用到webpack的插件:CommonsChunkPlugin
CommonsChunkPlugin有多种配制方式,常用的一种是你自己判断哪些文件是公用的,配制到入口文件中,如:
// 入口文件
entry: {
app: './src/index.js',
vendor: [
'react',
'react-dom'
]
},
...
plugins: [
// 将多个入口文件中公用的模块提取出来一个单独的文件,方便浏览器做缓存,可以有多个
new Webpack.optimize.CommonsChunkPlugin({
// 若什么都不配制,只配制一个公用模块的名字,则会把所有【入口文件(entry中配制的入口文件)】依赖的公用模块都提取到公用模块中
name: ['vendor', 'manifest'],
// ?还没明白这个参数的用法
// names: ['lodash', 'test'],
// filename: 'vender.[hash:4].bundle.js',
// 如: 3,指定当有几个文件共用的模块才需要提取,当Infinity保证只打指定的文件进来
minChunks: Infinity,
// 指定需要提取哪些入口文件中的公用模块
// chunks: [],
// 公共文件的文件大小的最小值
minSize: 1024
}),
]
更多配制参考:https://doc.webpack-china.org/plugins/commons-chunk-plugin/
另外,也可以不指定公用vendor文件,而是配制minChunks参数,指定当模块重复使用大于多少时提取
6、处理图片,及使用SCSS、autoprefixer处理CSS
我们的项目决定使用scss来做CSS的预处理,且希望使用autoprefixer使框架自动对CSS属性加上指定浏览器的兼容。我们使用webpack的loader来解决这些问题。
- 安装需要的loader
yarn add sass-loader node-sass css-loader style-loader --dev
yarn add postcss-loader autoprefixer --dev
// url-loader处理图片
yarn add url-loader --dev
注:
sass-loader:处理scss语法
node-sass:sass-loader的依赖
css-loader:CSS模块化解析,主要可以处理CSS中的@import 和 url()
style-loader:将JS中引入的CSS文件插入到HTML文件的header的style标签中
postcss-loader:处理CSS的一个平台,有很多基于他的插件,这里主要是用来使用autoprefixer
autoprefixer:对CSS属性加上指定浏览器的兼容
url-loader: 处理图片的加载,且可以指定小于多少的图片自动转成baseURI格式。
- 修改、配制webpack.config.js
...
module: {
rules: [
...
{
// .scss或.css文件使用下面的loader
test: /\.(scss|css)$/,
// loader使用顺序,postcss-loader --> sass-loader --> css-loader --> style-loader
use: ['style-loader', 'css-loader', 'sass-loader', {
loader: 'postcss-loader',
options: {
plugins: [require('autoprefixer')]
}
}]
},
{
// 处理图片格式的文件
test: /\.(png|jpe?g||git)$/,
use: [{
loader: 'url-loader',
options: {
// 小于8192K的图片转成baseURI
limit: 8192
}
}]
},
...
]
}
注:use指定处理.scss或.css文件的loader,loader的执行顺序从右向左,即先使用postcss-loader处理,再使用sass-loader,依次向左执行
- 添加autoprefixer需要的配制文件
autoprefixer的配制文件有两种形式:一种是在项目根目录下创建一个.browserslistrc文件;一种是在package.json文件中添加配制。
我们选择将配制添加到package.json文件中,如下:
{
...
"browserslist": [
"Android > 4",
"IOS > 5"
],
...
}
注:更多browserslist的配制参考:https://github.com/ai/browserslist#config-file
7、使用react-router处理路由
在react项目中,react-router已经是一个比较成熟的路由管理工具,我们可以直接使用。这里,我们使用react-router 4.1.2版本。官方帮助文档地址:https://reacttraining.com/react-router/web/guides/philosophy
如果不清楚我们为什么要使用路由管理工具,可以自己试着不用react-router,自己实现当有几个页面时,根据不同的地址来切换页面内容。再使用react-router,你就能体会到方便之处了。
- 安装react-router
版本4的react-router分成了好几个包,如下:
我们只需要安装react-router、react-router-dom:
yarn add react-router react-router-dom --save
- 使用
react-router的官方文档(https://reacttraining.com/react-router/web/guides/philosophy)中有很详细的使用帮助,介意有时间可以自己按照文档及例子学习。
这里需要说明一点,我们使用的版本4与之前的版本使用方式上还是有挺多不同的,比如我们项目中使用hash来做路由,我们的代码如下:
/**App.js*/
import React, { Component } from 'react'
import ReactDOM from 'react-dom'
// 这里使用react-router-dom中的HashRouter,4之前的版本引用方法是:
// import { Router, Route } from 'react-router'
// Router是根路由,由history属性来决定使用哪一种路由方式
import { HashRouter, Route, Switch } from 'react-router-dom'
// 引入页面,在src目录的module目录下创建你的页面
import Page1 from './module/Page1'
import Page2 from './module/Page2'
import NotFoundPage from './module/404'
class App extends Component {
render() {
return (
<HashRouter>
{/* Switcth组件作用是:只展示第一个匹配到的路由页面内容 */}
<Switch>
<Route path="/page1" component={Page1}></Route>
<Route path="/page2" component={Page2}></Route>
<Route component={NotFoundPage}></Route>
</Switch>
</HashRouter>
)
}
}
export default App
8、页面JS按需求加载
按需加载,其实在react-router的官方文档中也叫代码分离(code-splitting)。若按照我们上面的步骤做下来,APP.js中引用的page1,page2,404这三个页面的代码都会打包到一起,这显然不是我们想要的效果。我们需要访问一个具体的页面时,只加载当前页面的JS。好在react-router 4的文档中有一个非常详细的例子(https://reacttraining.com/react-router/web/guides/code-splitting)。我们需要使用到webpack的bundle-loader来异步加载每一个页面的JS。
- 安装bundle-loader
yarn add bundle-loader
注:可以先学习使用bundle-loader,https://doc.webpack-china.org/loaders/bundle-loader/可以更好的帮助我们理解接下来要做的事情
- 改造APP.js
1). 首先使用bundle-loader加载页面JS, 以Page1为例:
import Page1 from 'bundle-loader?lazy!./module/Page1'
注:bundle-loader后面的?lazy是该loader接收的参数,表示使用懒加载来加载Page1.js
若你看过bundle-loader的使用文档,你就会知道,此时使用lazy加载进来的Page1并不是真正的页面组件,而只是一个加载器,只有真正调用Page1时,才会去加载Page1.js,如:
Page1((file) => {
// 我们使用的是webpack3,所以需要加default才能拿到真正的组件
return file.default
})
2). 创建一个Bunlde.js用来统一处理bundle-loader import进来的loader,方便react-router的Route组件的componet使用
/**Bundle.js*/
import React, {
Component
} from 'react'
class Bundle extends Component {
state = {
// 要加载的module
mod: null
}
componentWillMount() {
this.load(this.props)
}
load(props) {
this.setState({
mod: null
})
// 这里的load就是我们通过bundle-load?lazy加载进来的
props.load((mod) => {
this.setState({
mod: mod.default || mod
})
})
}
render() {
return this.state.mod ? this.props.children(this.state.mod) : null
}
}
export default Bundle
3). APP.js中使用Bundle.js
/**App.js*/
import React, { Component } from 'react'
import ReactDOM from 'react-dom'
// 这里使用react-router-dom中的HashRouter,4之前的版本引用方法是:
// import { Router, Route } from 'react-router'
// Router是根路由,由history属性来决定使用哪一种路由方式
import { HashRouter, Route, Switch } from 'react-router-dom'
import Bundle from './Bundle'
// 引入页面
import Page1 from 'bundle-loader?lazy!./module/Page1'
import Page2 from 'bundle-loader?lazy!./module/Page2'
import NotFoundPage from 'bundle-loader?lazy!./module/404'
// 处理bundle-loader?lazy加载进来的模块的方法,供Route的componet属性使用
function lazyLoad(mod) {
return (props) => (
<Bundle load={mod}>
{(Mod) => <Mod {...props} />}
</Bundle>
)
}
class App extends Component {
render() {
return (
<HashRouter>
{/* Switcth组件作用是:只展示第一个匹配到的路由页面内容 */}
<Switch>
<Route path="/page1" component={lazyLoad(Page1)}></Route>
<Route path="/page2" component={lazyLoad(Page2)}></Route>
<Route component={lazyLoad(NotFoundPage)}></Route>
</Switch>
</HashRouter>
)
}
}
export default App
4). 修改入口文件index.js
/**index.js*/
import React from 'react'
import ReactDOM from 'react-dom'
// import './styles/index.scss'
import App from './App'
ReactDOM.render(
<App />,
document.getElementById('root')
)
9、组件热更新
以上我们的工程基本搭建完成,而为了提高我们的开发速度,组件热更新肯定少不了。目前我们修改工程中的JS文件,浏览器中打开的页面会自动全局刷新,而组件热更新的意思是,不刷新整个页面,只更新修改的组件对应的DOM。
我们需要用到react-hot-loader,同时webpack-dev-server也要开户热更新的功能。
- 安装react-hot-loader
yarn add react-hot-loader --save
注:同样,你可以不使用react-hot-loader,先尝试webpack官方文档中的原生热更新的例子来加深理解:https://doc.webpack-china.org/guides/hot-module-replacement/
react-hot-loader是webpack推荐的react模块热更新组件,更多的使用文档可以参考:https://github.com/gaearon/react-hot-loader/tree/master/docs
- 改造我们的代码
1). webpack.config.js文件修改内容:
...
entry: {
// 1)
app: ['react-hot-loader/patch', './src/index.js'],
vendor: [
'react',
'react-dom',
'react-hot-loader',
'react-router-dom'
]
}
...
devServer: {
// 2) 启用HMR
hot: true
}
...
moudle: {
rules: [
...
{
test: /\.jsx?$/,
exclude: /node_modules/,
use: [
// 使用babel-loader解析
{
loader: 'babel-loader',
options: {
presets: ['react', 'es2015'],
plugins: [
// 3) react-hot-loader for HMR
'react-hot-loader/babel',
'transform-class-properties'
]
}
}
]
},
...
]
}
...
plugins: [
// 4) 启用HMR
new Webpack.HotModuleReplacementPlugin(),
]
2). 修改index.js
import React from 'react'
import ReactDOM from 'react-dom'
// 使用react-hot-loader包裹所有的组件
import {
AppContainer
} from 'react-hot-loader'
// import './styles/index.scss'
import App from './App'
const render = (Component) => {
ReactDOM.render(
<AppContainer>
<Component />
</AppContainer>,
document.getElementById('root')
)
}
render(App);
// 用于监听react模块的热更新
if (module.hot) {
module.hot.accept('./App', () => {
render(require('./App').default)
})
}
10、生产环境图片、CSS、js处理、以及生成打包信息文件
生产环境想要对图片、CSS、js打包时进行压缩,且图片小于某个指定的大小时可以自动转换成baseUri格式。
生产打包时,我们可以有专门的生产webpack的配制文件与开发环境的区分开,可以使用如下的:
/** webpack.deploy.js */
const Webpack = require('webpack');
const path = require('path');
// HTML文件模板解析插件
const HtmlWebpackPlugin = require('html-webpack-plugin');
// 提取CSS文件到单独的文件
const ExtractTextPlugin = require('extract-text-webpack-plugin');
// 提取打包信息
const StatsPlugin = require('stats-webpack-plugin');
const env = process.env.NODE_ENV === 'product' ? 'product' : 'test'
const publicPaths = {
test: '',
product: '/'
};
module.exports = {
entry: {
app: ['./src/index.js'],
vendor: [
'react',
'react-dom',
'react-hot-loader',
'react-router-dom',
'react-weui'
]
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js',
// 这里使用chunkhash的好处是,当chunk中只有一个变化时,重新打包只会修改变化的chunk文件名的hash值
chunkFilename: '[id].[chunkhash:4].js',
publicPath: publicPaths[env]
},
// devtool: 'source-map',
module: {
rules: [{
test: /\.jsx?$/,
include: [path.resolve(__dirname, "src/module")],
use: ['bundle-loader?lazy'] // src/module下的文件都使用动态加载
}, {
test: /\.jsx?$/,
exclude: /node_modules/,
use: [
// 使用babel-loader解析
{
loader: 'babel-loader',
options: {
presets: ['react', 'es2015'],
plugins: [
'transform-class-properties'
]
}
}
]
}, {
test: /\.(scss|css)$/,
use: ExtractTextPlugin.extract({
fallback: 'style-loader',
use: ['css-loader', 'sass-loader', {
loader: 'postcss-loader',
options: {
plugins: [require('autoprefixer')]
}
}]
})
}, {
test: /\.(png|jpe?g||git)$/,
use: [{
loader: 'url-loader',
options: {
limit: 8192
}
}, {
loader: 'image-webpack-loader',
options: {
progressive: true,
nterlaced: false,
pngquant: {
quality: '65-90',
speed: 4
}
}
}]
}]
},
plugins: [
// 提取公用的JS vendor
new Webpack.optimize.CommonsChunkPlugin({
name: ['vendor', 'manifest'],
minChunks: Infinity,
}),
new HtmlWebpackPlugin({
title: 'webpack react study',
template: 'src/index.html'
}),
new ExtractTextPlugin('app.css'),
new Webpack.optimize.UglifyJsPlugin({
// 生成源文件,方便调试
// sourceMap: true
}),
// 更多参数参考http://webpack.github.io/docs/node.js-api.html#stats-tojson
new StatsPlugin('webpack.stats.json', {
// the source code of modules
source: false,
// built modules information
modules: true
}),
new Webpack.DefinePlugin({
'process.env.NODE_ENV': process.env.NODE_ENV || 'test'
})
]
}
项目中需要安装如下包:
// ExtractTextPlugin提取CSS单独打包
yarn add ExtractTextPlugin --dev
// image-webpack-loader处理图片
yarn add image-webpack-loader --dev
// stats-webpack-plugin输出打包信息
yarn add stats-webpack-plugin --dev
特别说明: image-webpack-loader处理图片若报错,本地需要全局安装libpng,安装方式:brew install libpng
stats-webpack-plugin配制中指定输出webpack.stats.json文件,该文件可以导入https://chrisbateman.github.io/webpack-visualizer/中查看项目打包时每一个模块的大小,可以给我们打包优化做参考
导入到visualizer平台上的stats文件效果如下: