从零开始搭建React脚手架

1.初始化项目

yarn init

生成一个最基础的package.json文件

{
  "name": "react-scaffold",
  "version": "1.0.0",
  "description": "React脚手架",
  "main": "index.js",
  "license": "MIT"
}

2.依赖安装

yarn add react --save // react框架安装
yarn add react-dom --save // react-dom安装
yarn add react-router-dom --save // react-router-dom安装
yarn add less --dev // less包

yarn add express // express服务
yarn add webpack --dev // 打包工具webpack
yarn add webpack-cli --dev // webpack cli工具
yarn add webpack-merge --dev // 合并webpack配置文件工具
yarn add html-webpack-plugin --dev // 自动生成打包后的html文件
yarn add css-loader style-loader less-loader --dev // css解析loader
yarn add url-loader file-loader --dev // 图片处理loader
yarn add webpack-dev-middleware --dev // 自动打包
yarn add webpack-hot-middleware --dev // 热更新
yarn add uglifyjs-webpack-plugin --dev // 压缩文件
yarn add optimize-css-assets-webpack-plugin --dev // 压缩css文件

yarn add babel-loader --dev // webpack中需要用到的loader.
yarn add @babel/core --dev // 用于解析最新ES标准
yarn add @babel/polyfill --dev // 用于解析最新ES语法api
yarn add @babel/plugin-transform-runtime --dev //  
yarn add @babel/runtime --dev //babel transform runtime 插件的依赖.
yarn add @babel/preset-env --dev // 根据预设的环境自动选用合适babel插件
yarn add @babel/preset-es2015 --dev // 用于转化 ES6
yarn add @babel/preset-react --dev // 用于解析 JSX

3.babel配置

Babel将最新标准的javaScript语法向下编译至ES5版本

在根目录下新建文件babelrc

{
  "presets": [
    ["@babel/env", {
      // 指定需要兼容的浏览器类型和版本
      "targets": {
        "browsers": ["last 2 versions"]
      }
    }],
    "@babel/react" // 解析jsx
  ]
}

4.eslint配置

yarn add eslint babel-eslint eslint-plugin-babel eslint-config-prettier eslint-plugin-react --dev // eslint工具
yarn add prettier eslint-plugin-prettier --dev // 设置代码风格插件

新建eslintrc,配置eslint规则

也可以新建.eslintignore文件,配置eslint忽略的文件

{
  "env": {
    "es6": true,
    "browser": true,
    "node": true
  },
  "parser": "babel-eslint",
  "extends": [
    "prettier", // 代码风格统一工具
    "plugin:react/recommended", // 支持react
  ],
  "plugins": [
    "babel",
    "react",
    "prettier"
  ],
  "parserOptions": {
    "ecmaVersion": 6,
    "sourceType": "module",
    "ecmaFeatures": {
      "jsx": true
    }
  },
  "rules": {
    "prettier/prettier": "error" // // 被prettier标记的地方抛出错误信息
  }
}

新建.prettierrc.js,设置代码风格规则

module.exports = {
  printWidth: 80, //一行的字符数,如果超过会进行换行,默认为80
  tabWidth: 2, //一个tab代表几个空格数,默认为80
  useTabs: false, //是否使用tab进行缩进,默认为false,表示用空格进行缩减
  singleQuote: false, //字符串是否使用单引号,默认为false,使用双引号
  semi: true, //行位是否使用分号,默认为true
  trailingComma: 'all', //是否使用尾逗号,有三个可选值"<none|es5|all>"
  bracketSpacing: true, //对象大括号直接是否有空格,默认为true,效果:{ foo: bar }
}

5.webpack

在package.json中配置脚本命令

通过不同的命令执行开发或生产环境的打包

"scripts": {
    "build:dev": "webpack --config ./webpack.dev.js",
    "build:prod": "webpack --config ./webpack.prod.js"
  },

5.1webpack公用配置

根目录下新建webpack-common-js文件

const path = require("path")
const webpack = require('webpack')
const HtmlwebpackPlugin = require('html-webpack-plugin')

// __dirname是node中的一个全局变量,它指向当前执行脚本所在的目录
const ROOT_PATH = path.resolve(__dirname)
const HTML_PATH = path.resolve(ROOT_PATH, 'src/index.html')
const BUILD_PATH = path.resolve(ROOT_PATH, 'dist')

module.exports = {
  output: {
    path: BUILD_PATH, // bundle输出目录
    chunkFilename: "bundle.[hash].js", // bundle生成文件的名称
    publicPath: '/',
  },
  resolve: {
    extensions: [".ts", ".tsx", ".js", ".jsx", ".json"], // // 自动补全后缀
    alias: {
        '@': path.resolve('src'), // 相对路径配置
      },
  },
  module: {
    rules: [
       // 将js或者jsx结尾的文件,使用babel解析
      {
        test: /\.(js|jsx)?$/,
        exclude: /node_modules/,
        use: ['babel-loader'],
      },
      // css解析loader
      {
        test: /\.(less|css)$/,
        include: [/src/],
        use: [
          require.resolve('style-loader'),
          {
            loader: require.resolve('css-loader'),
          },
          {
            loader: require.resolve('less-loader'),
          },
        ],
      },
      // 图片处理loader
      {
        test: /\.(png|jpg|gif)$/,
        use: [{
            loader: 'url-loader',
            options: {
                limit: 8192 // 小于等于8K的图片会被转成base64编码,直接插入HTML中,减少HTTP请求
            }
        }]
        },
    ]
  },
  // 插件
  plugins: [
    //在build目录下自动生成index.html
    new HtmlwebpackPlugin({
      template: HTML_PATH,
      filename: 'index.html',
    }),
  ]
}

5.2webpack开发环境配置

const merge = require('webpack-merge');
const path = require("path")
const webpack = require('webpack')
const common = require('./webpack.common.js');

const APP_PATH = path.resolve(__dirname, 'src/index.jsx')

module.exports = merge(common, {
  mode: 'development', // 开发环境模式
  // 入口地址
  entry: [
    "webpack-hot-middleware/client?path=/__webpack_hmr",
    "@babel/polyfill",
    APP_PATH
  ],
  //启用source-map方便调试,将错误消息位置映射到模块
  devtool: 'source-map',
  plugins: [
    new webpack.HotModuleReplacementPlugin(), // 热更新插件
  ]
})
5.2.1自动打包与热更新配置

webpack 可以通过watch mode方式来监听资源变化来进行自动打包操作,但是每次打包后的结果将会存储到本地硬盘中,而 IO 操作是非常耗资源时间的,无法满足本地开发调试需求。

webpack-dev-middleware 拥有以下几点特性:

以 watch mode 启动 webpack ,监听的资源一旦发生变更,便会自动编译,生产最新的 bundle

在编译期间,停止提供旧版的 bundle 并且将请求延迟到最新的编译结果完成之后

webpack 编译后的资源会存储在内存中,当用户请求资源时,直接于内存中查找对应资源,减少去硬盘中查找的 IO 操作耗时

新建server.js文件

通过启动一个 express服务,将 webpackDevMiddleware()的结果通过 app.use 方法注册为 express 服务的中间函数。webpackDevMiddleware()的执行结果返回的是一个 express 的中间件。它作为一个容器,将 webpack 编译后的文件存储到内存中,然后在用户访问 express 服务时,将内存中对应的资源输出返回。

const path = require("path")
const express = require("express")
const webpack = require("webpack")
const webpackDevMiddleware = require("webpack-dev-middleware")
const webpackConfig = require('./webpack.config.js')

const app = express()
const DIST_DIR = path.join(__dirname, "dist") // 设置静态访问文件路径
const complier = webpack(webpackConfig)  // webpack options
// webpackDevMiddleware options
const devMiddlewareOptions = {
  publicPath: webpackConfig.output.publicPath, //绑定中间件的公共路径,与webpack配置的路径相同
  hot: true, // 热更新
}
app.use(webpackDevMiddleware(complier, devMiddlewareOptions))

app.use(express.static(DIST_DIR)) 

app.listen(8000, function(){
    console.log("成功启动:localhost:"+ 8000)
})

然后在package.json中添加

"scripts": {
    "start": "node server.js"
 },

webpack-hot-middleware中间件将其本身安装为Webpack插件,并监听编译器事件。

每个连接的客户端都会获得Server Sent Events连接,服务器将向编译器事件的连接客户端发送通知。

当客户端收到消息时,它将检查本地代码是否是最新的。如果不是最新的,它将触发webpack热模块重新加载。

// server.js
const webpackHotMiddleware = require('webpack-hot-middleware')
app.use(webpackHotMiddleware(complier))

// webpack.config.js
plugins: [
    new webpack.HotModuleReplacementPlugin()
]

// 项目入口处 src/index.jsx
if (module.hot) {
    module.hot.accept();
}

配置完webpack-dev-middlewarewebpack-hot-middleware后,webpack就能够自动打包,热更新页面了。

5.3webpack生产环境配置

const merge = require('webpack-merge')
const path = require("path")
const UglifyJSPlugin = require('uglifyjs-webpack-plugin')
const common = require('./webpack.common.js')

module.exports = merge(common, {
  mode: 'production', // 生产环境模式
  entry: {
    main: path.resolve(__dirname, "src/index.jsx")
  },
  devtool: 'inline-source-map',
  optimization: {
    splitChunks: {
        chunks: 'all' // 第三方库分离
    },
    minimizer: [
      new UglifyJSPlugin({
        uglifyOptions: {
          compress: {
            unused: true,
            dead_code: true,
            drop_console: true,
            drop_debugger: true,
            pure_funcs: ['console.log']
          },
        },
        sourceMap: true,
      }),
    ],
  },
  performance: {
    hints: "warning", // 抛出提示信息类型
    maxAssetSize: 30000000, // 入口起点的最大体积
    maxEntrypointSize: 50000000, // 生成文件的最大体积
    assetFilter: function(assetFilename) {
      // 只给出 js、css 文件的性能提示
      return assetFilename.endsWith('.css') || assetFilename.endsWith('.js');
    },
  },
  plugins: []
})

5.3.1代码压缩
const UglifyJSPlugin = require('uglifyjs-webpack-plugin')
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')

module.exports = {
    ...
  optimization: {
    minimizer: [
      new UglifyJSPlugin({
        uglifyOptions: {
          compress: {
            unused: true, // 删除未引用的函数和变量
            dead_code: true, // 删除无法访问的代码
            drop_console: true, // 放弃对console.*函数的调用
            drop_debugger: true, // 删除debugger语句
            pure_funcs: ['console.log'] // 移除console
          },
        },
      }),
      new OptimizeCSSAssetsPlugin({}), // css压缩
    ],
  },
}

6. React

6.1React解析

浏览器不能解析React中的JSX语法,需要安装babel来解析:
yarn add babel-loader --dev
再安装用于解析es6和jsx语法的插件:
yarn add @babel/preset-env @babel/preset-es2015 @babel/preset-react --dev

以及React框架

yarn add react react-dom --save

还需在.babelrc进行相关配置

{
  "presets": [
    ["@babel/env", {
      "targets": {
        "browsers": ["last 2 versions"]
      }
    }],
    "@babel/react"
  ]
}

6.2React-router+按需加载

安装yarn add react-router-dom --save依赖

按需加载,也就是代码拆分,通过路由来判断何时来加载对应模块代码,缩短首页渲染时间

import React, { lazy, Suspense } from "react"
import { BrowserRouter, Switch, Route } from 'react-router-dom'

const Component1 = lazy(() => import(/* webpackChunkName: "Component1" */ '../components/Component1'))
const Component2 = lazy(() => import(/* webpackChunkName: "Component2" */ '../components/Component2'))

function App() {
  return (
    <Suspense fallback={<div>Loading</div>}>
      <BrowserRouter>
        <Switch>
            <Route path="/component2" component={Component2}/>
            <Route path="/" component={Component1}/>
        </Switch>
      </BrowserRouter>
    </Suspense>
  )
}
export default App

然后在webpack配置中加上chunkFilename

output: {
    chunkFilename: "[name].[hash].js", // bundle生成文件的名称
  },

这样打包就能够生存单独的模块代码,实现按需加载

6.3React局部更新

前面已经实现了webpack热更新,但是在React中,state也会被重新刷新,如果想保存state,需要一些插件的帮助

  1. 安装yarn add react-hot-loader @hot-loader/react-dom --dev

  2. 配置babelrc

    {
     ...
     "plugins": ["react-hot-loader/babel"]
    }
    
  3. 更新webpack.dev.js

    resolve: {
        alias: {
          'react-dom': '@hot-loader/react-dom'
        }
    },
    
  4. 在组件中引入react-hot-loader

    import { hot } from 'react-hot-loader/root'
    ...
    export default hot(App)
    

6.4React-reudx基本配置

安装yarn add redux react-redux --save

store配置

src/controller下创建store.js文件

import { createStore } from 'redux'
import reducer from '.reducer'

const store = createStore(reducer)
export default store

reducer配置

src/controller下创建reducer.js文件

reducer已拆分,需引入各个模块下的reducer

import { combineReducers } from 'redux'
import { reducer as Component1Reducer } from '../../components/Component1/controller'

const reducer = combineReducers({
  component1: Component1Reducer,
})

export default reducer

然后在项目入口处用Provider将所有组件包裹起来

import { Provider } from 'react-redux'
import store from '../controller/store' // 总的store
<Provider store={store}>
    ...
</Provider>

然后在各个模块下建立独立的reducer

// types
export const types = {
  TITLE_CHANGE: 'TITLE_CHANGE',
}
// actionTypes
import { types } from './types'
export function titleChange() {
    return {type: types.TITLE_CHANGE}
}
// reducer
import { types } from './types'
const defaultState = {
  title: 'hello',
}
export default (state = defaultState, action) => {
  switch (action.type) {
    case types.TITLE_CHANGE:
      const title = action.payload
      return { ...state, title }
    default:
      return state
  }
}
// index.js 将action,reduecer等导出
import reducer from './reducer'
import { types as component1Types } from './types'
import { actionTypes as component1ActionTypes } from './actionTypes'
export { reducer, component1Types, component1ActionTypes }

6.5reducer动态添加

在Redux中,reducer 决定了最后的整个状态的数据结构,在 store 中有一个 replaceReducer(nextReducer)方法,它是 Redux 中的一个高阶 API ,该函数接收一个 nextReducer 参数,用于替换 store 中原有的 reducer ,以此可以改变 store 中原有的状态的数据结构

src/controller/reducer

import store from '../store'

const allReducer = (asyncReducers) => {
  return combineReducers({
    ...asyncReducers,
  })
}

// 在 store 中动态注入 reducer 
const injectReducer = (key, reducer) => {
  // 判断当前store中的asyncReducers是否存在该reducer
  if (Object.hasOwnProperty.call(store.asyncReducers, key)) return
  // 添加新增的reducer
  store.asyncReducers[key] = reducer
  // 替换原有的reducer
  store.replaceReducer(allReducer(store.asyncReducers))
}

然后在src/controller/store

const store = () => {
  // 传入的reducer是动态生成的
  const store = createStore(allReducer())
  // 增加了一个asyncReducer的属性对象,用来缓存旧的reducers,然后与新的reducer合并
  store.asyncReducers = {}
  return store
}

最后就在模块处调用injectReducer()处传入相应的key,和reducer就可以了

警告信息:redux.js:319 Store does not have a valid reducer. Make sure the argument passed to combineReducers is an object whose values are reducers.

出现该警告信息的时候,可能是combineReducers中的reducer值为空

可以设置一个初始reducer

// 初始state
const initialState = {}
// action对象
const ACTION_HANDLERS = {}
const rootReducer = (state = initialState, action) => {
  const handler = ACTION_HANDLERS[action.type]
  return handler ? handler(state, action) : state
}
const allReducer = (asyncReducers = {}) => {
  return combineReducers({
    ...asyncReducers,
    rootReducer,
  })
}

6.6Redux相关库/插件

6.6.1redux-actions

当项目较大时,action就会有很多,reducer中也就需要些大量的swich来对action.type进行判断。

redux-actions当出现就是了简化actionCreator,和reducer中的操作

  1. createAction

    // 创建action
    import { createAction } from 'redux-actions'
    const titleChange = createAction(types.TITLE_CHANGE)
    
  1. handleActions

    // 处理action的操作,返回一个reduce
    import { handleActions } from 'redux-actions'
    import { types } from './types'
    const state = { title: 'hello' }
    const reducer = {
      [types.TITLE_CHANGE]: (state, action) => {
        const title = action.payload
        return { ...state, title }
      },
    }
    handleActions(reducer, state)
    
6.6.2reselect

react-redux自身提供了connect函数,可以通过mapStateToProps获取state,但是当我们需要计算一些state时候,每计算一次就会去调用一次mapStateToProps。

因此,为了减少渲染压力,就产生了reselect,reselect最重要的就是运用了缓存机制。

  1. selector可以计算衍生的数据,让redux做到存储尽可能少的state。
  2. 并且只有在参数变化的时候才计算.
  3. selector是可以组合的,他们可以作为输入,传递到其他的selector.
// controller/selector 创建Selector 
import { createSelector } from 'reselect'
const component1Selector = (state) => {
  return state.component1
}
const title = createSelector(component1Selector, (state) => state.title)
export const selector = { title }

// controller/index 
import { selector as component1Selector } from './selector'
export { component1Selector }

// 引用Selector 
import { component1Selector, component1ActionTypes } from './controller'
const title = useSelector(component1Selector.title)
6.6.3redux-devtools-extension

redux调试工具

首先通过Chrome拓展程序安装Redux DevTools工具

store.js

const is_DEV = process.env.NODE_ENV === 'development'

const store = createStore(
  allReducer(),
  is_DEV &&
    window.__REDUX_DEVTOOLS_EXTENSION__ &&
    window.__REDUX_DEVTOOLS_EXTENSION__(),
)

配置完成就可以在Chrome控制台中查看Redux信息

6.6.3redux-observable

redux-observable,是redux的一个中间件库。redux-observable将触发的 action 转化为一个数据流,并且订阅它。可以在这些流上监听新值,可以对这些值做出反应,最后将这些流转成 action

安装yarn add rxjs redux-observable --save

首先在store中加载redux-observable中间件

import { applyMiddleware, createStore, compose } from 'redux'
import { createEpicMiddleware } from 'redux-observable'

const store = () => {
    ...
  const epicMiddleware = createEpicMiddleware()
  const middleware = [epicMiddleware]
  const enhancers = []

  if (is_DEV && window.__REDUX_DEVTOOLS_EXTENSION__) {
    enhancers.push(window.__REDUX_DEVTOOLS_EXTENSION__())
  }

  const store = createStore(
    allReducer(),
    compose(applyMiddleware(...middleware), ...enhancers),
  )
  ...
}

配置rootEpics

controller/middleware新建rootEpics.js

import { combineEpics } from 'redux-observable'
import { BehaviorSubject } from 'rxjs'
import { mergeMap } from 'rxjs/operators'

const epic$ = new BehaviorSubject(combineEpics())

export const rootEpics = (action$, state$) =>
  epic$.pipe(mergeMap((epic) => epic(action$, state$)))
// 动态生成Epics
export const injectEpics = (key, newEpics) => {
  epic$.next(newEpics)
}

store.js中添加

import { rootEpics } from './middleware'
const store = ..
// store后插入
epicMiddleware.run(rootEpics)

最后在各个模块下的epics文件中处理action

import { injectEpics } from '@/controller/middleware/rootEpics'
import { combineEpics, ofType } from 'redux-observable'
import { actionTypes } from './actionTypes'
import {  map } from 'rxjs/operators'

const titleChange = (action$) => {
  return action$.pipe(
    ofType(actionTypes.titleChange),
    map((action) => {
      return actionTypes.titleChangeSucc(action.payload)
    }),
  )
}
injectEpics('component1_epic', combineEpics(titleChange))

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,376评论 6 491
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,126评论 2 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,966评论 0 347
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,432评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,519评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,792评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,933评论 3 406
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,701评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,143评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,488评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,626评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,292评论 4 329
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,896评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,742评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,977评论 1 265
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,324评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,494评论 2 348

推荐阅读更多精彩内容