用webpack4从零开始构建react脚手架

使用脚手架

  1. git clone git@github.com:xiehaitao0229/react-wepack4-xht.git`
  2. `cd react-webpack4-xht
  3. `npm run dev` // 启动本地
  4. `npm run build` // 打包线上环境
  5. `npm run clean` // 清除线上环境打包出来的文件
  6. `npm run test` // 单元测试的工具库
  7. `npm run fix` // 修复eslint的写法
  8. `npm run format` // 格式化代码
  9. `npm run precommit` // commit 代码到git仓库的检查

webpack今年推出的4这个版本就一直关注很学习它,webpack4这个版本借鉴了parcel的零配置,打包速度变得更快,值得大家去跟进学习。

既然我们已经迎接了webpack4的到来了,那么就一起来使用一下,即使你没用过之前的版本,没关系,我们重新出发,将工作中常用到的配置写给大家来看

安装webpack

  • 需要先在项目中npm init初始化一下,生成package.json
  • 建议node版本安装到8.2以上
  1. // webpack4中除了正常安装webpack之外,需要再单独安一个webpack-cli
  2. npm i webpack webpack-cli -D

webpack是基于Node的

在项目下创建一个webpack.config.js(默认,可修改)文件来配置webpack

  1. module.exports = {
  2. entry: '', // 入口文件
  3. output: {}, // 出口文件
  4. module: {}, // 处理对应模块
  5. plugins: [], // 对应的插件
  6. devServer: {}, // 开发服务器配置
  7. mode: 'development' // 模式配置
  8. }

以上就是webpack的正常配置模块

启动devServer需要安装一下webpack-dev-server

  1. npm i webpack-dev-server -D

image.png

接下来我们按照项目的结构,我们就从0开始去写一下配置

  1. // webpack.config.js
  2. const path = require('path');
  3. module.exports = {
  4. entry: './src/index.js', // 入口文件
  5. output: {
  6. filename: 'bundle.js', // 打包后的文件名称
  7. path: path.resolve('dist') // 打包后的目录,必须是绝对路径
  8. }
  9. }

上面就可以说是实现了最简单的webpack配置了,那接下来就打包一下看看

image.png

配置执行文件

工作当中我们打包编译的时候一般都执行npm run dev这样的命令,既然是通过npm执行的命令,我们就应该找到package.json里的执行脚本去配置一下命令,这里如下图所示

image.png

npm run build就是我们打包后的文件,这是生产环境下,上线需要的文件

npm run dev是我们开发环境下打包的文件,当然由于devServer帮我们把文件放到内存中了,所以并不会输出打包后的dist文件夹

配置Html模板

文件都打包好了,但是我们在使用的时候不能在dist目录下去创建一个html文件,然后去引用打包后的js吧,这不合理,实际开发中也不会这样

我们需要实现html打包功能,可以通过一个模板实现打包出引用好路径的html来

这就需要用到一个常用的插件了,< html-webpack-plugin >,用之前我们来安一下它

  1. npm i html-webpack-plugin -D
  2. let path = require('path');
  3. // 插件都是一个类,所以我们命名的时候尽量用大写开头
  4. let HtmlWebpackPlugin = require('html-webpack-plugin');
  5. module.exports = {
  6. entry: './src/index.js',
  7. output: {
  8. // 添加hash可以防止文件缓存,每次都会生成4位的hash串
  9. filename: 'bundle.js',
  10. path: path.resolve('dist')
  11. },
  12. plugins: [
  13. // 通过new一下这个类来使用插件
  14. new HtmlWebpackPlugin({
  15. // 用哪个html作为模板
  16. // 在src目录下创建一个index.html页面当做模板来用
  17. template: './src/index.html',
  18. hash: true, // 会在打包好的bundle.js后面加上hash串
  19. })
  20. ]
  21. }

通过上面的配置后,我们再npm run build打包看一下现在是个什么样子了

image.png

多页面开发,怎么配置多页面

如果开发的时候不只一个页面,我们需要配置多页面,那么需要怎么来搞呢?不用担心,html-webpack-plugin插件自有办法,我们来观望一下

  1. let path = require('path');
  2. let HtmlWebpackPlugin = require('html-webpack-plugin');
  3. module.exports = {
  4. // 多页面开发,怎么配置多页面
  5. entry: {
  6. index: './src/index.js',
  7. login: './src/login.js'
  8. },
  9. // 出口文件
  10. output: {
  11. filename: '[name].js',
  12. path: path.resolve('dist')
  13. },
  14. plugins: [
  15. new HtmlWebpackPlugin({
  16. template: './src/index.html',
  17. filename: 'index.html',
  18. chunks: ['index'] // 对应关系,index.js对应的是index.html
  19. }),
  20. new HtmlWebpackPlugin({
  21. template: './src/index2.html',
  22. filename: 'login.html',
  23. chunks: ['login'] // 对应关系,login.js对应的是login.html
  24. })
  25. ]
  26. }

image.png

上面基本介绍完了html和js的打包配置了,webpack对css的解析需要用到loader,所以我们先提前安装好,待会好方便使用

引用CSS文件

需要下载一些解析css样式的loader

  1. npm i style-loader css-loader -D
  2. // 引入less文件的话,也需要安装对应的loader
  3. npm i less less-loader -D
  4. npm i node-sass sass-loader -D

下面我们来看一下如何配置css文件的解析

  1. // index.js
  2. import './css/style.css'; // 引入css
  3. import './less/style.less'; // 引入less
  4. console.log('这里是打包文件入口-index.js');
  5. // webpack.config.js
  6. module.exports = {
  7. entry: {
  8. index: './src/index.js'
  9. },
  10. output: {
  11. filename: 'bundle.js',
  12. path: path.resolve('dist')
  13. },
  14. module: {
  15. rules: [
  16. {
  17. test: /\.css$/, // 解析css
  18. use: ['style-loader', 'css-loader'] // 从右向左解析
  19. /*
  20. 也可以这样写,这种方式方便写一些配置参数
  21. use: [
  22. {loader: 'style-loader'},
  23. {loader: 'css-loader'}
  24. ]
  25. */
  26. }
  27. ]
  28. }
  29. }
  • 此时打包后的css文件是以行内样式style的标签写进打包后的html页面中,如果样式很多的话,我们更希望直接用link的方式引入进去,这时候需要把css拆分出来
  • extract-text-webpack-plugin插件它的功效就在于会将打包到js里的css文件进行一个拆分,单独提取css

拆分CSS

  1. // @next表示可以支持webpack4版本的插件
  2. npm i extract-text-webpack-plugin@next -D
  3. let path = require('path');
  4. let HtmlWebpackPlugin = require('html-webpack-plugin');
  5. // 拆分css样式的插件
  6. let ExtractTextWebpackPlugin = require('extract-text-webpack-plugin');
  7. module: {
  8. rules: [
  9. {
  10. test: /\.less$/, // 解析less
  11. use: ExtractTextWebpackPlugin.extract({
  12. // 将css用link的方式引入就不再需要style-loader了
  13. fallback: "style-loader",
  14. use: ['css-loader', 'less-loader'] // 从右向左解析
  15. })
  16. },
  17. {
  18. test: /\.scss$/, // 解析scss
  19. use: ExtractTextWebpackPlugin.extract({
  20. // 将css用link的方式引入就不再需要style-loader了
  21. fallback: "style-loader",
  22. use: ['css-loader', 'sass-loader'] // 从右向左解析
  23. })
  24. },
  25. {
  26. test: /\.css$/, // 解析css
  27. use: ExtractTextWebpackPlugin.extract({
  28. // 将css用link的方式引入就不再需要style-loader了
  29. fallback: "style-loader",
  30. use: ['css-loader']
  31. })
  32. }
  33. ]
  34. },
  35. plugins: [
  36. new HtmlWebpackPlugin({
  37. template: './src/index.html',
  38. }),
  39. // 拆分后会把css文件放到dist目录下的css/style.css
  40. new ExtractTextWebpackPlugin('css/style.css')
  41. ]

image.png

另一个插件mini-css-extract-plugin也是可以办到的,它可以说是为webpack4而生的,

在这里就简单的提一下

  1. npm i mini-css-extract-plugin -D
  2. let MiniCssExtractPlugin = require('mini-css-extract-plugin');
  3. module.exports = {
  4. module: {
  5. rules: [
  6. {
  7. test: /\.css$/,
  8. use: [MiniCssExtractPlugin.loader, 'css-loader']
  9. }
  10. ]
  11. },
  12. plugins: [
  13. new MiniCssExtractPlugin({
  14. filename: 'css/a.css' // 指定打包后的css
  15. })
  16. ]
  17. }

拆分成多个css

这里要着重说一下上面两个插件的区别了,个人还是建议用extract-text-webpack-plugin的,毕竟从之前的版本承接下来的,虽然在安包的时候需要@next,但是还是值得信赖的

而且现在的extract-text-webpack-plugin也支持了拆分成多个css,而目前mini-css-extract-plugin还不支持此功能

  1. // 正常写入的less
  2. let styleLess = new ExtractTextWebpackPlugin('css/style.css');
  3. // reset
  4. let resetCss = new ExtractTextWebpackPlugin('css/reset.css');
  5. module.exports = {
  6. module: {
  7. rules: [
  8. {
  9. test: /\.css$/,
  10. use: resetCss.extract({
  11. fallback: "style-loader",
  12. use: 'css-loader'
  13. })
  14. },
  15. {
  16. test: /\.less$/,
  17. use: styleLess.extract({
  18. fallback: "style-loader",
  19. use: ['css-loader', 'less-loader'] // 从右向左解析
  20. })
  21. }
  22. ]
  23. },
  24. plugins: [
  25. styleLess,
  26. resetCss
  27. ]
  28. }

通过这样操作后可以打包成两个不同的css文件,如下图

image.png

引用图片

  1. npm i file-loader url-loader -D

如果是在css文件里引入的如背景图之类的图片,就需要指定一下相对路径

  1. module.exports = {
  2. module: {
  3. rules: [
  4. {
  5. test: /\.(jpe?g|png|gif)$/,
  6. use: [
  7. {
  8. loader: 'url-loader',
  9. options: {
  10. limit: 8192, // 小于8k的图片自动转成base64格式,并且不会存在实体图片
  11. outputPath: 'images/' // 图片打包后存放的目录
  12. }
  13. }
  14. ]
  15. }
  16. ]
  17. }
  18. }

在css中指定了publicPath路径这样就可以根据相对路径引用到图片资源了,如下图所示

image.png

页面img引用图片

页面中经常会用到img标签,img引用的图片地址也需要一个loader来帮我们处理好

  1. npm i html-withimg-loader -D
  2. module.exports = {
  3. module: {
  4. rules: [
  5. {
  6. test: /\.(htm|html)$/,
  7. use: 'html-withimg-loader'
  8. }
  9. ]
  10. }
  11. }

这样再打包后的html文件下img就可以正常引用图片路径了

image.png

引用字体图片和svg图片

字体图标和svg图片都可以通过file-loader来解析

  1. module.exports = {
  2. module: {
  3. rules: [
  4. {
  5. test: /\.(eot|ttf|woff|svg)$/,
  6. use: 'file-loader'
  7. }
  8. ]
  9. }
  10. }

这样即使样式中引入了这类格式的图标或者图片都没有问题了,img如果也引用svg格式的话,配合上面写好的html-withimg-loader就都没有问题了

添加CSS3前缀

通过postcss中的autoprefixer可以实现将CSS3中的一些需要兼容写法的属性添加响应的前缀,这样省去我们不少的时间

由于也是一个loader加载器,我们也需要先安装一下

  1. npm i postcss-loader autoprefixer -D

安装后,我们还需要像webpack一样写一个config的配置文件,在项目根目录下创建一个postcss.config.js文件,配置如下:

  1. module.exports = {
  2. plugins: [
  3. require('autoprefixer')({
  4. "browsers": [
  5. "defaults",
  6. "not ie < 11",
  7. "last 2 versions",
  8. "> 1%",
  9. "iOS 7",
  10. "last 3 iOS versions"
  11. ]
  12. })
  13. ]
  14. };

然后在webpack里配置postcss-loader

  1. module.exports = {
  2. module: {
  3. rules: [
  4. {
  5. test: /\.less$/, // 解析less
  6. use: ExtractTextWebpackPlugin.extract({
  7. // 将css用link的方式引入就不再需要style-loader了
  8. fallback: "style-loader",
  9. use: ['css-loader', 'postcss-loader', 'less-loader'] // 从右向左解析
  10. })
  11. },
  12. {
  13. test: /\.scss$/, // 解析scss
  14. use: ExtractTextWebpackPlugin.extract({
  15. // 将css用link的方式引入就不再需要style-loader了
  16. fallback: "style-loader",
  17. use: ['css-loader', 'postcss-loader', 'sass-loader'] // 从右向左解析
  18. })
  19. },
  20. {
  21. test: /\.css$/, // 解析css
  22. use: ExtractTextWebpackPlugin.extract({
  23. // 将css用link的方式引入就不再需要style-loader了
  24. fallback: "style-loader",
  25. use: ['css-loader', 'postcss-loader']
  26. })
  27. },
  28. ]
  29. }
  30. }

转义ES6和react

在实际开发中,我们在大量的使用着ES6及之后的api去写代码,这样会提高我们写代码的速度,不过由于低版本浏览器的存在,不得不需要转换成兼容的代码,于是就有了常用的Babel了

Babel会将ES6的代码转成ES5的代码

  1. npm i babel-core babel-loader babel-preset-env babel-preset-stage-3 babel-preset-react babel-polyfill babel-plugin-import babel-loader babel-register -D
  2. babel-preset-stage-3 使用这个插件来编译为了后面使用...state扩展运算符可以使用

当把这些都安好后,我们就开始配置,由于要兼容的代码不仅仅包含ES6还有之后的版本和那些仅仅是草案的内容,所以我们可以通过一个.babelrc文件来配置一下,对这些版本的支持

  1. // .babelrc
  2. {
  3. "presets": [
  4. [
  5. "env",
  6. {
  7. "loose": true,
  8. "modules": false
  9. }
  10. ],
  11. "es2015",
  12. "react",
  13. "babel-preset-stage-3"
  14. ]
  15. }

我们再在webpack里配置一下babel-loader既可以做到代码转成ES5了

  1. module.exports = {
  2. module: {
  3. rules: [
  4. {
  5. test:/\.js$/,
  6. use: 'babel-loader',
  7. include: /src/, // 只转化src目录下的js
  8. exclude: /node_modules/ // 排除掉node_modules,优化打包速度
  9. }
  10. ]
  11. }
  12. }

加入 babel-plugin-transform-runtime 和 babel-polyfill

1.先来说说babel-plugin-transform-runtime

在转换 ES2015 语法为 ECMAScript 5 的语法时,babel 会需要一些辅助函数,例如 _extend。babel 默认会将这些辅助函数内联到每一个 js 文件里,这样文件多的时候,项目就会很大。

所以 babel 提供了 transform-runtime 来将这些辅助函数“搬”到一个单独的模块 babel-runtime 中,这样做能减小项目文件的大小。

  1. npm install --save-dev babel-plugin-transform-runtime

修改.babelrc配置文件,增加配置

.babelrc

  1. "plugins": [
  2. "transform-runtime"
  3. ]

2.再来看babel-polyfill

为什么要集成babel-polyfill?

Babel默认只转换新的JavaScript句法(syntax),而不转换新的API,比如Iterator、Generator、Set、Maps、Proxy、Reflect、Symbol、Promise等全局对象,以及一些定义在全局对象上的方法(比如Object.assign)都不会转码。

举例来说,ES6在Array对象上新增了Array.from方法。Babel就不会转码这个方法。如果想让这个方法运行,必须使用babel-polyfill,为当前环境提供一个垫片。

  1. npm install --save-dev babel-polyfill
  2. // 修改入口文件index.js
  3. import 'babel-polyfill';

呀呀呀,在我们每次npm run build的时候都会在dist目录下创建很多打好的包,如果积累过多可能也会混乱

所以应该在每次打包之前将dist目录下的文件都清空,然后再把打好包的文件放进去,主人们,接下来我们使用clean-webpack-plugin这个插件吧

  1. npm i clean-webpack-plugin -D
  2. let CleanWebpackPlugin = require('clean-webpack-plugin');
  3. module.exports = {
  4. plugins: [
  5. // 打包前先清空
  6. new CleanWebpackPlugin('dist')
  7. ]
  8. }

resolve解析

  1. resolve: {
  2. // 别名
  3. alias: {
  4. pages:path.join(__dirname,'src/pages'),
  5. component:path.join(__dirname,'src/component'),
  6. actions:path.join(__dirname,'src/redux/actions'),
  7. reducers:path.join(__dirname,'src/redux/reducers'),
  8. },
  9. // 省略后缀
  10. extensions: ['.js', '.jsx', '.json', '.css', '.scss', '.less']
  11. }

提取公共代码

在webpack4之前,提取公共代码都是通过一个叫CommonsChunkPlugin的插件来办到的。到了4以后,内置了一个一模一样的功能 optimization

下面我们就来看看如何提取公共代码

  1. optimization: {
  2. splitChunks: {
  3. cacheGroups: {
  4. vendor: { // 抽离第三方插件
  5. test: /node_modules/, // 指定是node_modules下的第三方包
  6. chunks: 'initial',
  7. name: 'vendor', // 打包后的文件名,任意命名
  8. // 设置优先级,防止和自定义的公共代码提取时被覆盖,不进行打包
  9. priority: 10
  10. },
  11. utils: {
  12. // 抽离自己写的公共代码,utils里面是一个公共类库
  13. chunks: 'initial',
  14. name: 'utils', // 任意命名
  15. minSize: 0 // 只要超出0字节就生成一个新包
  16. }
  17. }
  18. }
  19. },
  20. 还要在plugins里面引入需要单独打包出来的chunk
  21. new HtmlWebpackPlugin({
  22. template: './src/index.html',
  23. chunks: ['vendor', 'index', 'utils'] // 引入需要的chunk
  24. }),

image.png

webpack-dev-server

简单来说,webpack-dev-server就是一个小型的静态文件服务器。使用它,可以为webpack打包生成的资源文件提供Web服务。

  1. npm install webpack-dev-server --save-dev
  2. npm install webpack-dev-server -g
  3. devServer: {
  4. port: 3000, // 端口
  5. open: true, // 自动打开浏览器
  6. hot: true, // 开启热更新
  7. overlay: true, // 浏览器页面上显示错误
  8. historyApiFallback: true
  9. },

devtool优化

现在我们发现一个问题,代码哪里写错了,浏览器报错只报在build.js第几行。这让我们排查错误无从下手,传送门

在开发环境下配置

  1. devtool: 'inline-source-map'

同时,我们在srouce里面能看到我们写的代码,也能打断点调试代码

热更新和自动刷新的区别

在配置devServer的时候,如果hot为true,就代表开启了热更新,但是这并没有那么简单,因为热更新还需要配置一个webpack自带的插件并且还要在主要js文件里检查是否有module.hot

  1. // webpack.config.js
  2. let webpack = require('webpack');
  3. module.exports = {
  4. plugins: [
  5. // 热更新,热更新不是刷新
  6. new webpack.HotModuleReplacementPlugin()
  7. ],
  8. devServer: {
  9. hot: true, // 加上这一行
  10. }
  11. }
  12. // 在入口文件index.js
  13. // 还需要在主要的js文件里写入下面这段代码
  14. if (module.hot) {
  15. // 实现热更新
  16. module.hot.accept();
  17. }

热更新的好处在开发vue或者react的时候,其中某个组件修改的时候就会针对这个组件进行更新,超级好用,提升开发效率

集成react-router

  1. `npm install --save react-router-dom`

新建router文件夹和组件

  1. `cd src`
  2. `mkdir router && touch router/router.js`

按照react-router文档编辑一个最基本的router.js。包含两个页面homepage1

src/router/router.js

  1. import React from 'react';
  2. import {BrowserRouter as Router, Route, Switch, Link} from 'react-router-dom';
  3. import Home from '../pages/Home/Home';
  4. import Page1 from '../pages/Page1/Page1';
  5. const getRouter = () => (
  6. <Router>
  7. <div>
  8. <ul>
  9. <li><Link to="/">首页</Link></li>
  10. <li><Link to="/page1">Page1</Link></li>
  11. </ul>
  12. <Switch>
  13. <Route exact path="/" component={Home}/>
  14. <Route path="/page1" component={Page1}/>
  15. </Switch>
  16. </div>
  17. </Router>
  18. );
  19. export default getRouter;

新建页面文件夹

  1. cd src
  2. mkdir pages

新建两个页面 Home,Page1

  1. cd src/pages
  2. mkdir Home && touch Home/Home.js
  3. mkdir Page1 && touch Page1/Page1.js
  4. src/pages/Home/Home.js
  5. import React, {Component} from 'react';
  6. export default class Home extends Component {
  7. render() {
  8. return (
  9. <div>
  10. this is home ~hi xht
  11. </div>
  12. )
  13. }
  14. }
  15. Page1.js
  16. import React, {Component} from 'react';
  17. export default class Page1 extends Component {
  18. render() {
  19. return (
  20. <div>
  21. this is Page1~hi xht
  22. </div>
  23. )
  24. }
  25. }

现在路由和页面建好了,我们在入口文件src/index.js引用Router。

修改src/index.js

  1. import React from 'react';
  2. import ReactDom from 'react-dom';
  3. import getRouter from './router/router';
  4. ReactDom.render(
  5. getRouter(), document.getElementById('root'));

现在执行打包命令npm run dev。打开index.html查看效果啦!

集成react-redux

接下来,我们就要就要就要集成redux了。

要对redux有一个大概的认识,可以阅读阮一峰前辈的Redux 入门教程(一):基本用法

如果要对redux有一个非常详细的认识,我推荐阅读中文文档,写的非常好。读了这个教程,有一个非常深刻的感觉,redux并没有任何魔法。

我们就做一个最简单的计数器。自增,自减,重置。

先安装redux

  1. `npm install --save redux`

初始化目录结构

  1. cd src
  2. mkdir redux
  3. cd redux
  4. mkdir actions
  5. mkdir reducers
  6. touch reducers.js
  7. touch store.js
  8. touch actions/couter.js
  9. touch reducers/couter.js

先来写action创建函数。通过action创建函数,可以创建action

src/redux/actions/counter.js

  1. /*action*/
  2. export const INCREMENT = "INCREMENT";
  3. export const DECREMENT = "DECREMENT";
  4. export const RESET = "RESET";
  5. export function increment() {
  6. return {type: INCREMENT}
  7. }
  8. export function decrement() {
  9. return {type: DECREMENT}
  10. }
  11. export function reset() {
  12. return {type: RESET}
  13. }

再来写reducer,reducer是一个纯函数,接收action和旧的state,生成新的state.

src/redux/reducers/couter.js

  1. import { INCREMENT, DECREMENT, RESET } from '../actions/couters';
  2. /*
  3. * 初始化state
  4. */
  5. const initState = {
  6. count: 0,
  7. };
  8. /*
  9. * reducer
  10. */
  11. export default function reducer(state = initState, action) {
  12. switch (action.type) {
  13. case INCREMENT:
  14. return {
  15. count: state.count + 1,
  16. };
  17. case DECREMENT:
  18. return {
  19. count: state.count - 1,
  20. };
  21. case RESET:
  22. return { count: 0 };
  23. default:
  24. return state;
  25. }
  26. }

一个项目有很多的reducers,我们要把他们整合到一起

src/redux/reducers.js

  1. import counter from './reducers/couter';
  2. export default function combineReducers(state = {}, action) {
  3. return {
  4. counter: counter(state.counter, action)
  5. }
  6. }

reducer就是纯函数,接收state 和 action,然后返回一个新的 state。

看上面的代码,无论是combineReducers函数也好,还是reducer函数也好,都是接收state和action,

返回更新后的state。区别就是combineReducers函数是处理整棵树,reducer函数是处理树的某一点。

接下来,我们要创建一个store。

前面我们可以使用 action 来描述“发生了什么”,使用action创建函数来返回action。

还可以使用 reducers 来根据 action 更新 state 。

那我们如何提交action?提交的时候,怎么才能触发reducers呢?

store 就是把它们联系到一起的对象。store 有以下职责:

维持应用的 state;

  • 提供 getState() 方法获取 state;
  • 提供 dispatch(action) 触发reducers方法更新 state;
  • 通过subscribe(listener) 注册监听器;
  • 通过 subscribe(listener) 返回的函数注销监听器。
  1. src/redux/store.js
  2. import {createStore} from 'redux';
  3. import combineReducers from './reducers.js';
  4. let store = createStore(combineReducers);
  5. export default store;

写一个Counter页面

  1. cd src/pages
  2. mkdir Counter
  3. touch Counter/Counter.js
  4. src/pages/Counter/Counter.js
  5. import React, {Component} from 'react';
  6. export default class Counter extends Component {
  7. render() {
  8. return (
  9. <div>
  10. <div>当前计数为(显示redux计数)</div>
  11. <button onClick={() => {
  12. console.log('调用自增函数');
  13. }}>自增
  14. </button>
  15. <button onClick={() => {
  16. console.log('调用自减函数');
  17. }}>自减
  18. </button>
  19. <button onClick={() => {
  20. console.log('调用重置函数');
  21. }}>重置
  22. </button>
  23. </div>
  24. )
  25. }
  26. }

修改路由,增加Counter

src/router/router.js

  1. import React from 'react';
  2. import { HashRouter as Router, Route, Switch, Link } from 'react-router-dom';
  3. import Home from 'pages/Home/Home';
  4. import Page1 from 'pages/Page1/Page1';
  5. import Counter from 'pages/Counter/Counter';
  6. const getRouter = () => (
  7. <Router>
  8. <div>
  9. <ul>
  10. <li>
  11. <Link to="/">首页</Link>
  12. </li>
  13. <li>
  14. <Link to="/page1">Page1</Link>
  15. </li>
  16. <li>
  17. <Link to="/couter">Counter</Link>
  18. </li>
  19. </ul>
  20. <Switch>
  21. <Route exact path="/" component={Home} />
  22. <Route path="/page1" component={Page1} />
  23. <Route path="/couter" component={Counter} />
  24. </Switch>
  25. </div>
  26. </Router>
  27. );
  28. export default getRouter;

npm run dev 看看效果。

下一步,我们让Counter组件和react-redux联合起来。使Counter能获得到Redux的state,并且能发射action。

先来安装react-redux

  1. npm install --save react-redux
  2. src/pages/Counter/Counter.js
  3. import React, { Component } from 'react';
  4. import { increment, decrement, reset } from 'actions/couters';
  5. import { connect } from 'react-redux';
  6. class Counter extends Component {
  7. render() {
  8. const {
  9. counter: { count },
  10. increment,
  11. decrement,
  12. reset,
  13. } = this.props;
  14. return (
  15. <div>
  16. <div>
  17. 当前计数为:
  18. {count}
  19. </div>
  20. <button onClick={() => increment()}>自增</button>
  21. <button onClick={() => decrement()}>自减</button>
  22. <button onClick={() => reset()}>重置</button>
  23. </div>
  24. );
  25. }
  26. }
  27. const mapStateToProps = (state) => {
  28. return {
  29. counter: state.couter,
  30. };
  31. };
  32. const mapDispatchToProps = (dispatch) => {
  33. return {
  34. increment: () => {
  35. dispatch(increment());
  36. },
  37. decrement: () => {
  38. dispatch(decrement());
  39. },
  40. reset: () => {
  41. dispatch(reset());
  42. },
  43. };
  44. };
  45. export default connect(
  46. mapStateToProps,
  47. mapDispatchToProps
  48. )(Counter);

下面我们要传入store

所有容器组件都可以访问 Redux store,所以可以手动监听它。一种方式是把它以 props 的形式传入到所有容器组件中。但这太麻烦了,因为必须要用 store 把展示组件包裹一层,仅仅是因为恰好在组件树中渲染了一个容器组件。

建议的方式是使用指定的 React Redux 组件 来 让所有容器组件都可以访问 store,而不必显示地传递它。只需要在渲染根组件时使用即可。

  1. src/index.js
  2. import React from 'react';
  3. import ReactDOM from 'react-dom';
  4. import './css/index';
  5. import { Provider } from 'react-redux';
  6. import getRouter from './router/router';
  7. import store from './redux/store';
  8. const router = getRouter();
  9. /* 初始化 */
  10. renderWithHotReload(router);
  11. function renderWithHotReload(RootElement) {
  12. ReactDOM.render(
  13. <Provider store={store}>{RootElement}</Provider>,
  14. document.getElementById('root')
  15. );
  16. }
  17. // 还需要在主要的js文件里写入下面这段代码
  18. if (module.hot) {
  19. // 实现热更新
  20. module.hot.accept();
  21. }

我们在说清楚一下

1.Provider组件是让所有的组件可以访问到store。不用手动去传。也不用手动去监听。

2.connect函数作用是从 Redux state 树中读取部分数据,并通过 props 来把这些数据提供给要渲染的组件。也传递dispatch(action)函数到props。

引入异步action,集成redux-thunk

下面,我们以向后台请求用户基本信息为例。

1.我们先创建一个user.json,等会请求用,相当于后台的API接口。

  1. 在根目录
  2. mkdir api
  3. cd api
  4. touch user.json
  5. user.json
  6. {
  7. "name": "xiehaitao",
  8. "intro": "please give me a star"
  9. }

2.创建必须的action创建函数。

  1. cd src/redux/actions
  2. touch userInfo.js
  3. src/redux/actions/userInfo.js
  4. export const GET_USER_INFO_REQUEST = "GET_USER_INFO_REQUEST";
  5. export const GET_USER_INFO_SUCCESS = "GET_USER_INFO_SUCCESS";
  6. export const GET_USER_INFO_FAIL = "GET_USER_INFO_FAIL";
  7. function getUserInfoRequest() {
  8. return {
  9. type: GET_USER_INFO_REQUEST
  10. }
  11. }
  12. function getUserInfoSuccess(userInfo) {
  13. return {
  14. type: GET_USER_INFO_SUCCESS,
  15. userInfo: userInfo
  16. }
  17. }
  18. function getUserInfoFail() {
  19. return {
  20. type: GET_USER_INFO_FAIL
  21. }
  22. }

我们创建了请求中,请求成功,请求失败三个action创建函数。

3.创建reducer

  1. cd src/redux/reducers
  2. touch userInfo.js
  3. src/redux/reducers/userInfo.js
  4. import { GET_USER_INFO_REQUEST, GET_USER_INFO_SUCCESS, GET_USER_INFO_FAIL } from 'actions/userInfo';
  5. const initState = {
  6. isLoading: false,
  7. userInfo: {},
  8. errorMsg: '',
  9. };
  10. export default function reducer(state = initState, action) {
  11. switch (action.type) {
  12. case GET_USER_INFO_REQUEST:
  13. return {
  14. ...state,
  15. isLoading: true,
  16. userInfo: {},
  17. errorMsg: '',
  18. };
  19. case GET_USER_INFO_SUCCESS:
  20. return {
  21. ...state,
  22. isLoading: false,
  23. userInfo: action.userInfo,
  24. errorMsg: '',
  25. };
  26. case GET_USER_INFO_FAIL:
  27. return {
  28. ...state,
  29. isLoading: false,
  30. userInfo: {},
  31. errorMsg: '请求错误',
  32. };
  33. default:
  34. return state;
  35. }
  36. }

组合reducer

src/redux/reducers.js

  1. import counter from 'reducers/counter';
  2. import userInfo from 'reducers/userInfo';
  3. export default function combineReducers(state = {}, action) {
  4. return {
  5. couter: couter(state.couter, action),
  6. userInfo: userInfo(state.userInfo, action)
  7. }
  8. }

4.现在有了action,有了reducer,我们就需要调用把action里面的三个action函数和网络请求结合起来。

src/redux/actions/userInfo.js增加

  1. export function getUserInfo() {
  2. return function (dispatch) {
  3. dispatch(getUserInfoRequest());
  4. return fetch('/api/user.json')
  5. .then((response) => {
  6. return response.json();
  7. })
  8. .then((json) => {
  9. dispatch(getUserInfoSuccess(json));
  10. })
  11. .catch(() => {
  12. dispatch(getUserInfoFail());
  13. });
  14. };
  15. }

我们这里发现,别的action创建函数都是返回action对象:

但是我们现在的这个action创建函数 getUserInfo则是返回函数了。

为了让action创建函数除了返回action对象外,还可以返回函数,我们需要引用redux-thunk。

  1. npm install --save redux-thunk

简单的说,中间件就是action在到达reducer,先经过中间件处理。我们之前知道reducer能处理的action只有这样的{type:xxx},所以我们使用中间件来处理

函数形式的action,把他们转为标准的action给reducer。这是redux-thunk的作用。

使用redux-thunk中间件

src/redux/store.js

  1. import { createStore, applyMiddleware } from 'redux';
  2. import thunkMiddleware from 'redux-thunk';
  3. import combineReducers from './reducers';
  4. const store = createStore(combineReducers, applyMiddleware(thunkMiddleware));
  5. export default store;

到这里,redux-thunk已经配置完成了,写一个组件来验证一下

  1. cd src/pages
  2. mkdir UserInfo
  3. cd UserInfo
  4. touch UserInfo.js
  5. src/pages/UserInfo/UserInfo.js
  6. import React, { Component } from 'react';
  7. import { connect } from 'react-redux';
  8. import { getUserInfo } from 'actions/userInfo';
  9. class UserInfo extends Component {
  10. render() {
  11. const {
  12. userInfo: { userInfo, isLoading, errorMsg },
  13. getUserInfo,
  14. } = this.props;
  15. return (
  16. <div>
  17. {isLoading
  18. ? '请求信息中......'
  19. : errorMsg || (
  20. <div>
  21. <p>用户信息:</p>
  22. <p>
  23. 用户名:
  24. {userInfo.name}
  25. </p>
  26. <p>
  27. 介绍:
  28. {userInfo.intro}
  29. </p>
  30. </div>
  31. )}
  32. <button onClick={() => getUserInfo()}>请求用户信息</button>
  33. </div>
  34. );
  35. }
  36. }
  37. export default connect(
  38. state => ({ userInfo: state.userInfo }),
  39. { getUserInfo }
  40. )(UserInfo);

增加路由

src/router/router.js

  1. import React from 'react';
  2. import { HashRouter as Router, Route, Switch, Link } from 'react-router-dom';
  3. import Home from 'pages/Home/Home';
  4. import Page1 from 'pages/Page1/Page1';
  5. import Counter from 'pages/Counter/Counter';
  6. import UserInfo from 'pages/UserInfo/UserInfo';
  7. const getRouter = () => (
  8. <Router>
  9. <div>
  10. <ul>
  11. <li>
  12. <Link to="/">首页</Link>
  13. </li>
  14. <li>
  15. <Link to="/page1">Page1</Link>
  16. </li>
  17. <li>
  18. <Link to="/couter">Counter</Link>
  19. </li>
  20. <li>
  21. <Link to="/userinfo">UserInfo</Link>
  22. </li>
  23. </ul>
  24. <Switch>
  25. <Route exact path="/" component={Home} />
  26. <Route path="/page1" component={Page1} />
  27. <Route path="/couter" component={Counter} />
  28. <Route path="/userinfo" component={UserInfo} />
  29. </Switch>
  30. </div>
  31. </Router>
  32. );
  33. export default getRouter;

image.png

combinReducers优化

redux提供了一个combineReducers函数来合并reducer,不用自己合并

src/redux/reducers.js

  1. import { combineReducers } from 'redux';
  2. import userInfo from 'reducers/userInfo';
  3. import couter from './reducers/couters';
  4. export default combineReducers({
  5. couter,
  6. userInfo,
  7. });

指定环境

在webpack4之前都是需要建三个文件来需要环境的,webpack.base.js,webpack.dev.js,webpack.prod.js,现在在webpack4之后就不需要了,因为用--mode 就可以区分环境了

先安装

  1. npm install -D yargs-parser
  2. 这个包可以拿到--mode 里面的参数,这样子就可以区别是本地环境还是线上环境了
  3. "dev": "cross-env webpack-dev-server --mode development",
  4. "build": "npm run lint && cross-env npm run clean && webpack --mode production",
  5. // webpack.config.js
  6. const argv = require('yargs-parser')(process.argv.slice(2));
  7. const pro = argv.mode == 'production' ? true : false; // 区别是生产环境和开发环境
  8. let plu = [];
  9. if (pro) {
  10. // 线上环境
  11. plu.push(
  12. new HtmlWebpackPlugin({
  13. template: './src/index.html',
  14. hash: true, // 会在打包好的bundle.js后面加上hash串
  15. chunks: ['vendor', 'index', 'utils'] // 引入需要的chunk
  16. }),
  17. // 拆分后会把css文件放到dist目录下的css/style.css
  18. new ExtractTextWebpackPlugin('css/style.[chunkhash].css'),
  19. new ExtractTextWebpackPlugin('css/reset.[chunkhash].css'),
  20. new CleanWebpackPlugin('dist'),
  21. )
  22. } else {
  23. // 开发环境
  24. plu.push(
  25. new HtmlWebpackPlugin({
  26. template: './src/index.html',
  27. chunks: ['vendor', 'index', 'utils'] // 引入需要的chunk
  28. }),
  29. // 拆分后会把css文件放到dist目录下的css/style.css
  30. new ExtractTextWebpackPlugin('css/style.css'),
  31. new ExtractTextWebpackPlugin('css/reset.css'),
  32. new webpack.HotModuleReplacementPlugin(), // 热更新,热更新不是刷新
  33. )
  34. }
  35. devtool: pro ? '' : 'inline-source-map' // 只有本地开发才需要调试

集成eslint

eslint目标是以可扩展,每条规则独立,不内置编码风格为理念的lint工具,用户可以定制自己的规则做成公共包

eslint主要有以下特点:

1)默认规则包含所有的jslint,jshint中存在的规则易迁移

2)规则可配置性高,可设置警告,错误两个error等级,也可以直接禁用

3)包含代码风格检测的规则

4)支持插件扩展,自定义规则

针对react开发者,eslint已经可以很好的支持jsx语法了。

先安装插件

  1. npm install -D eslint eslint-config-airbnb eslint-loader eslint-plugin-import eslint-plugin-jsx-a11y eslint-plugin-react

配置.eslintrc文件

  1. // 直接继承airbnb的配置规则,同时也可以写入自己的特定规则,后面的内容会覆盖默认的规则,
  2. // 下面是我比较习惯的lint规则
  3. {
  4. "extends": ["airbnb"],
  5. "env": {
  6. "browser": true,
  7. "node": true,
  8. "es6": true,
  9. "mocha": true,
  10. "jest": true,
  11. "jasmine": true
  12. },
  13. "rules": {
  14. "no-plusplus": [0],
  15. "eqeqeq": [0],
  16. "no-empty": [0],
  17. "no-param-reassign": [0],
  18. "generator-star-spacing": [0],
  19. "consistent-return": [0],
  20. "no-shadow": [0],
  21. "react/forbid-prop-types": [0],
  22. "react/jsx-filename-extension": [
  23. 1,
  24. {
  25. "extensions": [".js"]
  26. }
  27. ],
  28. "react/button-has-type": [
  29. "<enabled>",
  30. {
  31. "button": false,
  32. "submit": false,
  33. "reset": false
  34. }
  35. ],
  36. "global-require": [1],
  37. "import/prefer-default-export": [0],
  38. "react/jsx-boolean-value": [0],
  39. "react/jsx-no-bind": [0],
  40. "react/prop-types": [0],
  41. "react/prefer-stateless-function": [0],
  42. "react/jsx-one-expression-per-line": [0],
  43. "react/jsx-wrap-multilines": [
  44. "error",
  45. {
  46. "no-empty": [0],
  47. "no-param-reassign": [0],
  48. "declaration": "parens-new-line",
  49. "assignment": "parens-new-line",
  50. "return": "parens-new-line",
  51. "arrow": "parens-new-line",
  52. "condition": "parens-new-line",
  53. "logical": "parens-new-line",
  54. "prop": "ignore"
  55. }
  56. ],
  57. "no-else-return": [0],
  58. "no-restricted-syntax": [0],
  59. "import/no-extraneous-dependencies": [0],
  60. "no-use-before-define": [0],
  61. "jsx-a11y/no-static-element-interactions": [0],
  62. "jsx-a11y/no-noninteractive-element-interactions": [0],
  63. "jsx-a11y/click-events-have-key-events": [0],
  64. "jsx-a11y/anchor-is-valid": [0],
  65. "no-nested-ternary": [0],
  66. "arrow-body-style": [0],
  67. "import/extensions": [0],
  68. "no-bitwise": [0],
  69. "no-cond-assign": [0],
  70. "import/no-unresolved": [0],
  71. "comma-dangle": [
  72. "error",
  73. {
  74. "arrays": "always-multiline",
  75. "objects": "always-multiline",
  76. "imports": "always-multiline",
  77. "exports": "always-multiline",
  78. "functions": "ignore"
  79. }
  80. ],
  81. "object-curly-newline": [0],
  82. "function-paren-newline": [0],
  83. "no-restricted-globals": [0],
  84. "require-yield": [1]
  85. },
  86. "globals": {
  87. "document": true,
  88. "localStorage": true,
  89. "window": true
  90. }
  91. }

除此之外还要在webpack.config.js文件增加module的loader

  1. module: {
  2. rules: [
  3. {
  4. enforce: "pre", // 代表在解析loader之前就先解析eslint-loader
  5. test: /\.js$/,
  6. exclude: /node_modules/,
  7. include:/src/,
  8. loader: "eslint-loader",
  9. },
  10. ]
  11. }

pagekage.json文件里面script增加

  1. "lint": "npm run format && npm run fix && eslint --ext .js src", // 检测你写的代码是否符合eslint的规则
  2. "fix": "npm run format && eslint --fix --ext .js src", // npm run fix 这个是可以修复你没有按照lint规则的写法

自动格式化以及提交代码时的优化配置

第一步 格式化所有代码 prettier

  1. npm install -D prettier

在package.json的script里面添加如下配置

  1. {
  2. "scripts": {
  3. "format": "prettier --single-quote --trailing-comma es5 --write \"src/**/*.js\""
  4. }
  5. }

你可以通过 npm run format试一下是否可以自动格式化你的代码

第二步 配置Eslint

上面我们已经配置好eslint了在package.json的scripts里添加如下

  1. "fix": "npm run format && eslint --fix --ext .js src",

第三步 添加Git钩子(Pre-commit Hook)

Git 钩子(hooks)是在Git 仓库中特定事件(certain points)触发后被调用的脚本。 详情可浏览 https://git-scm.com/book/zh/v2/自定义-Git-Git-钩子

每次提交代码,执行 git commit之后进行自动格式化,免去每次人为手动格式化,使远程仓库代码保持风格统一。

  1. npm install -D lint-staged husky

在package.json的scripts里添加如下

  1. "precommit": "npm run lint",

现在让我们来看看在package.json的scripts的所有配置吧

  1. "scripts": {
  2. "dev": "cross-env webpack-dev-server --mode development",
  3. "build": "npm run lint && cross-env npm run clean && webpack --mode production",
  4. "precommit": "npm run lint",
  5. "clean": "cross-env rm -rf dist && mkdir dist",
  6. "test": "mocha --compilers js:babel-register --require ./test/test_helper.js --recursive",
  7. "test:watch": "npm run test --watch",
  8. "lint": "npm run format && npm run fix && eslint --ext .js src",
  9. "fix": "npm run format && eslint --fix --ext .js src",
  10. "format": "prettier --single-quote --trailing-comma es5 --write \"src/**/*.js\""
  11. },

用webpack4从零开始构建react脚手架的更多相关文章

  1. webpack4构建react脚手架

    create-react-app 脚手架还没有更新到webpack4,但是猛然间发现webpack4已经到 v4.12.0 版本了!!!慌得不行,正好端午有空所以研究了一波,自己搭建了一个简单的rea ...

  2. 【webpack系列】从零搭建 webpack4+react 脚手架(一)

    搭建一个React工程的方式有很多,官方也有自己的脚手架,如果你和我一样,喜欢刨根究底,从零开始自己一行一行代码创建一个React脚手架项目,那你就来对地方了.本教程是针对React新手,以及对web ...

  3. 【webpack系列】从零搭建 webpack4+react 脚手架(四)

    经过三个章节的学习,你已经学会搭建了一个基于webpack4的react脚手架.如果要更改配置,比如,你希望把编译后的js文件和css文件等单独放dist下的static目录下,你想想,是不是有点麻烦 ...

  4. 【webpack系列】从零搭建 webpack4+react 脚手架(二)

    html文件如何也同步到dist目录?bundle.js文件修改了,万一被浏览器缓存了怎么办?如何为导出的文件加md5?如何把js引用自动添加到html?非业务代码和业务代码如何分开打包?如何搭建开发 ...

  5. react native系列 - 从零开始构建

    从零开始构建第一步,当然是从hello world开始,第一课我们没什么代码都不写,只用生成的代码来打包apk.为什么一开始就要学会打包,因为如果连打包都不会,以后做好了也没用.学会了打包,才能让我们 ...

  6. React脚手架创建一个React应用以及项目目录结构详解

    react脚手架 用来帮助程序员快速创建一个基于xxx库的模板项目,包含了所有需要的配置,指定好了所有的依赖,可以直接安装/编译/运行一个简单效果 react提供了一个专门用于创建react项目的脚手 ...

  7. 简述--构建React项目的几种方式

    前言: 构建React项目的几种方式: 构建:create-react-app 快速脚手架 构建:generator-react-webpack 构建:webpack一步一步构建 1)构建:creat ...

  8. React学习系列之(1)简单的demo(React脚手架)

    1.什么是React? React是一个一个声明式,高效且灵活的用于构建用户界面的JavaScript库.React 起源于 Facebook 的内部项目,用来架设 Instagram 的网站,并于 ...

  9. 从零开始构建一个的asp.net Core 项目

    最近突发奇想,想从零开始构建一个Core的MVC项目,于是开始了构建过程. 首先我们添加一个空的CORE下的MVC项目,创建完成之后我们运行一下(Ctrl +F5).我们会在页面上看到"He ...

随机推荐

  1. Web框架的原理和Django初识

    一.Web框架的本质 1.本质 实际上Web应用本质上就是一个socket服务端, 而用户的浏览器就是一个socket客户端. 2.最原始的web框架 socket服务端 import socket ...

  2. [HDU5969] 最大的位或

    题目类型:位运算 传送门:>Here< 题意:给出\(l\)和\(r\),求最大的\(x|y\),其中\(x,y\)在\([l,r]\)范围内 解题思路 首先让我想到了前面那题\(Bits ...

  3. Git让你从入门到精通,看这一篇就够了

    简介 Git 是什么? Git 是一个开源的分布式版本控制系统. 什么是版本控制? 版本控制是一种记录一个或若干文件内容变化,以便将来查阅特定版本修订情况的系统. 什么是分布式版本控制系统? 介绍分布 ...

  4. 【系统架构】软件核心复杂性应对之道-领域驱动DDD(Domain-Driven Design)

    前言 领域驱动设计是一个开放的设计方法体系,目的是对软件所涉及到的领域进行建模,以应对系统规模过大时引起的软件复杂性的问题,本文将介绍领域驱动的相关概念. 一.软件复杂度的根源 1.业务复杂度(软件的 ...

  5. Window下Eclipse+Tomcat远程调试

    需求:       项目在开发环境跑得好好的,但是当发布到服务器上时,却出现了一些意外的问题.服务器上不可能给你装IDE调试工具啊,又没有很好的日志帮助下,这时候就用到了JVM的Java Platfo ...

  6. CMDB服务器管理系统【s5day91】:如何实现允许临时修改主机名

    一.sn号唯一 & 如何实现允许临时修改主机名 1.物理机 1.sn,物理机唯一 2.后台管理: 买服务器,清单:SN号,硬盘,内存... 作业:python 读取excel,xldt 3.资 ...

  7. Hadoop记录-metastore jmx配置

    参考:http://www.lixiuliang.cn/categories/%E5%A4%A7%E6%95%B0%E6%8D%AE/ 1.修改bin/hive文件 添加jvm启动参数: if [ $ ...

  8. js检测移动设备并跳转到相关适应页面。

    PS:网页自适应的方式有多种.有通过CSS样式表来实现自适应(主流),也有通过显示不同的页面来实现的方式. 下面代码是记录通过判断设备特征来跳转到相关的页面的方法. 实现要求: 当手机,平板访问 a. ...

  9. Web常见漏洞修复建议

    一.SQL注入修复建议 1.过滤危险字符,例如:采用正则表达式匹配union.sleep.and.select.load_file等关键字,如果匹配到则终止运行. 2.使用预编译语句,使用PDO需要注 ...

  10. 2.11 while循环的嵌套以及应用(难)

    while循环嵌套 前面学习过if的嵌套了,想一想if嵌套是什么样子的? 类似if的嵌套,while嵌套就是:while里面还有while <1>while嵌套的格式 while 条件1: ...