webpack-react-framework

主要介绍React项目环境如何配置,项目如何架构的。更多查看github: github.com/dzfrontend/…

项目如何架构的

技术栈

webpack + react + react-router4 + mobx + react服务端渲染

1.工程架构

Webpack基础配置

webpack官方文档:webpackjs.org webpack打包初始化:

安装npm i webpack --save -dev
在webpack/webpack.config.client.js里写好配置
package.json的"scripts"添加 "build": "webpack --config webpack/webpack.config.client.js"
运行npm run build

webpack/webpack.config.client.js:

const path = require('path')
module.exports = {
    // 入口文件
    entry: {
        app: path.join(__dirname, '../client/app.js')
    },
    // 打包文件
    output: {
        filename: '[name].[hash].js', //打包文件名,ame为entry的name
        path: path.join(__dirname, '../dist'), //打包路径
        publicPath: 'public' //加上前缀 
    }
}
复制代码

运行npm run build后,将client/app.js打包生成dist/app.hash.js

Webpack loader基础应用

配置打包react的loader:

处理jsx文件,安装npm i babel-loader -D(--save -dev)

babel支持es6语法,安装babel-loader后想要支持jsx,还要在根目录新建babel的配置文件.babelrc

此外还要安装npm i babel-core babel-preset-es2015 babel-preset-es2015-loose babel-preset-react -D

.babelrc配置:presets为要配置支持的语法

{
  "presets": [
    ["es2015", { "loose": true }],
    "react"
  ]
}
复制代码

webpack/webpack.config.client.js:

const path = require('path')
const HTMLPlugin = require('html-webpack-plugin')
module.exports = {
    ...entry,
	...output,
    module: {
        rules: [
            {
                test: /.jsx$/, //匹配后缀为jsx的文件
                loader: 'babel-loader' // 编译loader
            },
            {
                test: /.js$/,
                loader: 'babel-loader',
                exclude: [
                    path.join(__dirname,'../node_modules') //排除的文件夹
                ]
            }
        ]
    }
}
复制代码

安装html-webpack-plugin,自动生成index.html文件并且把打包文件注入html里面

module.exports = {
	...entry,
	...output,
	...module,
	plugins: [
        new HTMLPlugin({
            template: path.join(__dirname, '../client/template.html') //指定模板文件
        }) // 安装html-webpack-plugin插件,自动生成index.html文件(如果不存在指定模板文件时),并且把打包文件注入html里面
    ]
}
复制代码

这样就简单配置成功打包react的应用啦。

Webpack-dev-server

本地服务器和自动编译打包的作用 npm i webpack-dev-server -D

webpack/webpack.config.client.js 下面为webpack-dev-server的配置

const config = {
    ...entry,
    ...output,
    ...module,
    plugins
}
const isDev = process.env.NODE_ENV === 'development' //用于命令行运行package.json设置的环境变量的判断

// 如果运行的环境变量为'development',则启用webpack-dev-server
if(isDev){
    config.devServer = {
        host: '0.0.0.0', //可以使用任何方式访问 => 本地ip/localhost/127.0.0.1
        port: '8888', //端口号
        contentBase: path.join(__dirname, '../dist'), // 本地服务器的访问路径
        // hot: true, // hot-module-replacement是否启动,即热加载,需要安装react-hot-loader
        overlay: {errors: true},
        publicPath: '/public', //和webpack里output publicPath对应一样,不然加载文件的路径不对
        historyApiFallback: {
            index: '/public/index.html' //所有请求的不存在页面到这里来
        }
    }
}

module.exports = config
复制代码

接下来在package.json的"scripts"配置环境变量为development的命令。

其中设置环境变量为webpack中process.env.NODE_ENV === 'development'配置的话,需要安装npm i cross-env -D,然后cross-env NODE_ENV=development就设置了环境变量为'development'

"dev:client": "cross-env NODE_ENV=development webpack-dev-server --config webpack/webpack.config.client.js"
复制代码

在本地开发时,命令行工具运行npm run dev:client,就运行了webpack-dev-server,浏览器访问http://localhost:8888/.

实现局部热加载 npm i react-hot-loader -D

在.babelrc文件加上

{
  "presets":...,
  "plugins": ["react-hot-loader/babel"]
}
复制代码

在webpack/webpack.config.client.js里加上

if(isDev){
    config.entry = { //热加载打包入口文件增加打包文件'react-hot-loader/patch'
        app: [
            'react-hot-loader/patch',
            path.join(__dirname, '../client/app.js')
        ]
    }
    ...config.devServer
    config.plugins.push(new webpack.HotModuleReplacementPlugin()) //热加载加入pugins里
}

复制代码

在入口文件写为

import React from 'react'
import ReactDom from 'react-dom'
import App from './App.jsx'
import { AppContainer } from 'react-hot-loader' // 热加载

// ReactDom.render(<App/>,document.getElementById('root'))

// 热加载配置引入的组件加<AppContainer>包裹
const render = (Comment) => {
    ReactDom.render(
        <AppContainer>
            <Comment />
        </AppContainer>,
        document.getElementById('root')
    )
}
render(App)
if(module.hot){
    module.hot.accept('./App.jsx',()=>{
        const App = require('./App.jsx').default
        render(App)
    })
}
复制代码

react服务端渲染基础配置

React如何使用服务端渲染:react-dom/server用于服务端将react组件渲染成html。

搭建一个nodejs服务器,通过ReactSSR.renderToString将服务端渲染的内容替换本地的内容,来达到服务端返回解析的内容

npm i express -S

server/server.js:

const express = require('express')
const ReactSSR = require('react-dom/server')
const fs = require('fs')
const path = require('path')

const app = express()

// react服务端webpack配置webpack.config.server.js运行打包后的文件
const serverEntry = require('../dist/server-entry').default

// html打包后的模板,用来插入服务端渲染后的html
const template = fs.readFileSync(path.join(__dirname, '../dist/index.html'), 'utf8') //readFileSync同步读取

/* 
 * 静态文件请求
 * 指定../dist为静态文件目录,'/public'为webpack里output的publicPath路径,请求带有'/public'表示为静态资源请求,指向'../dist'目录
*/
app.use('/public', express.static(path.join(__dirname, '../dist'))) 

// 服务端请求
app.get('*', function(req,res){
	const appString = ReactSSR.renderToString(serverEntry)
	res.send(template.replace('<app></app>', appString))
})

app.listen(3333, function () {
  console.log('server is listening on 3333')
})
复制代码

接着需要配置服务端渲染的前端入口文件client/server-entry.js,和webpack打包配置webpack/webpack.config.server.js

client/server-entry.js

import React from 'react'
import App from './App.jsx'

// ReactDom.render(<App/>,document.body)
// 服务端没有dom节点,所以服务端渲染需要重新新建一个入口文件,并用export default组件的方式导出
export default <App />
复制代码

webpack/webpack.config.server.js和之前配置类似,入口文件改为了server-entry.js

/**
 * 服务端渲染加的webpack配置
 */
 const path = require('path')
 const HTMLPlugin = require('html-webpack-plugin')

 module.exports = {
 	target: 'node', //target表示执行环境为node
 	entry: {
 		app: path.join(__dirname, '../client/server-entry.js')
 	},
 	output: {
 		filename: 'server-entry.js', //server端没有用hash
 		path: path.join(__dirname, '../dist'), //打包路径
        publicPath: '/public/', //前缀
        libraryTarget: 'commonjs2' //server端commonjs规范,适用于服务端
 	},
	module: {
        rules: [
            {
                test: /.jsx$/, //匹配后缀为jsx的文件
                loader: 'babel-loader' // 编译loader
            },
            {
                test: /.js$/,
                loader: 'babel-loader',
                exclude: [
                    path.join(__dirname,'../node_modules') //排除的文件夹
                ]
            }
        ]
    }
 }
复制代码

在package.json配置命令打包入口文件和服务端渲染的前端入口文件

其中清除文件的命令"clear"需要安装npm i rimraf -D

{
  "scripts": {
    "build:client": "webpack --config webpack/webpack.config.client.js",
    "build:server": "webpack --config webpack/webpack.config.server.js",
    "clear": "rimraf dist", //清除dist文件夹内容
    "build": "npm run clear && npm run build:client && npm run build:server", // 打包入口文件和服务端渲染的前端入口文件
    "start": "node server/server.js"
  }
}
复制代码

命令行工具运行下面命令,react服务端渲染基础配置完成

npm run build // 同时打包
npm run start // 启动服务端渲染
复制代码

react服务端渲染本地环境配置

和react服务端渲染基础配置类似,只是本地开发环境用的是webpack-dev-server,没有生成本地打包文件;解决方案是通过axios请求本地服务器的资源 + webpack编译webpack.config.server.js。具体实现本地服务端渲染代码在server/util/dev-static.js,而server/server.js里会判断在'development'环境执行dev-static.js。

在package.json里

"scripts": {
  "dev:client": "cross-env NODE_ENV=development webpack-dev-server --config webpack/webpack.config.client.js",
  "dev:server": "cross-env NODE_ENV=development node server/server.js",
}
复制代码

运行npm run dev:client 首先启动本地开发环境服务器webpck-dev-server
运行npm run dev:server 启动本地服务端渲染
访问 http://localhost:3333 查看index.html里面的div id="root"里面有内容,说明本地服务端渲染配置成功

使用eslint和editconfig规范代码

eslint

作用:规范代码
.eslintrc文件为eslint的配置文件
rules里面可以定义一些忽略规则;在代码里想要忽略检查可以加上eslint-disable-line。

安装的插件详见代码。

根目录.eslintrc:全局eslint

{
  "extends": "standard" //标准的规则
}
复制代码

client/.eslintrc:react的eslint规则

其中rules里面可以定义一些忽略规则,添加了忽略规则不会报相应错误提示

{
  // 解析器(解析js)
  "parser": "babel-eslint",
  "env": {
    "browser": true, // 执行环境为browser,包含window对象
    "es6": true,
    "node": true
  },
  "parserOptions": {
    "ecmaVersion": 6,
    "sourceType": "module"
  },
  // airbnb规则,适用于react
  "extends": "airbnb",
  "rules": {
    //不写分号
    "semi": [0],
    // 报linebreak-style错误忽略
    "linebreak-style": 0,
    // 不能写在js而是jsx忽略
    "react/jsx-filename-extension": [0],
    // 缩进忽略
    "indent": [0]
  }
}
复制代码

配置了.eslintrc文件还不够,还需要在webpack.config里面的rules加上eslint配置

module: {
    rules: [
        {
            // eslint配置
            enforce: 'pre', //在执行rules之前
            test: /.(js|jsx)$/,
            loader: 'eslint-loader',
            exclude: [
              path.resolve(__dirname, '../node_modules')
            ]
        }
    ]
},
复制代码
editconfig

编辑器配置插件,vscode和sublime需要安装EditorConfig插件,.editorconfig配置文件才有效。

.editorconfig

root = true // 项目根目录

[*]
charset = utf-8
indent_style = space
indent_size = 4
end_of_line = lf
insert_final_newline = true // 末尾自动添加一行空行
trim_trailing_whitespace = true // 末尾去掉空格
复制代码
eslint正确git才能提交

安装 npm i husky -D

在package.json

"scripts":{
  "precommit": "eslint --ext .js --ext .jsx"
}
复制代码

在执行git commit之前,会执行precommit命令,只有eslint正确才能提交。

2.项目架构

React-Router4路由配置

npm i react-router react-router-dom -S

react-router4使用的时候只需引用 react-router-dom,如果搭配 redux,你还需要使用 react-router-redux

先配置路由地址:

import { BrowserRouter, Route, Redirect } from 'react-router-dom'

ReactDOM.render(
  (<BrowserRouter>
    <App> {/* app组件 */}
        <Route key="index" path="/" render={() => <Redirect to="/list" />} exact />
        <Route key="list" path="/list" component={TopicList} />
        <Route key="detail" path="/detail" component={TopicDetail} />
    </App>
  </BrowserRouter>),
  document.getElementById('root')
);

// app组件中
class App extends Component {
  render() {
    return (
      {this.props.children} // 将配置路由渲染
    );
  }
}
复制代码

其中**< Route />必须要< BrowserRouter >包裹**,Redirect为重定向

配置好路由后就可以直接通过指定的path路径访问

import { Link } from 'react-router-dom'

class App extends Component {
  render() {
    return (
      <div>
    	<Link to="/">首页</Link>
    	<Link to="/list">列表页</Link>
    	<Link to="/detail">详情页</Link>
      </div>
    );
  }
}
复制代码

Mobx

和redux的作用类似,文档cn.mobx.js.org/

安装mobx和mobx-react: npm i mobx mobx-react -S

mobx的流程如下,和redux的流程很相似;mobx的Computed相当于redux的Reducer,mobx的Reaction相当于redux的Store.

webpack环境还需要配置.babel文件; 安装所需插件npm i babel-plugin-transform-decorators-legacy babel-preset-stage-1 -D

.babel:在"presets"里加上"state-1","plugins"里加上"transform-decorators-legacy"

{
  "presets": [
    ...
    "state-1"
  ],
  "plugins": ["transform-decorators-legacy"]
}
复制代码

mobx的使用:

在app-state.js里面定义好action,state,computed

import { observable, computed, action } from 'mobx'

class AppState {
    constructor({ count, name } = { count: 0, name: 'Jack' }) {
        this.count = count
        this.name = name
    }
    // observable定义state
    @observable count
    @observable name

    // computed
    @computed get msg() {
        return `${this.name} say count is ${this.count}`
    }

    // action
    @action add() {
      this.count += 1
    }
    @action changeName(name) {
      this.name = name
    }
}

const appState = new AppState()

setInterval(() => {
    appState.add()
}, 1000)
export default appState
复制代码

然后将app-state.js绑定到根组件:

import { Provider } from 'mobx-react'
import appState from './store/app-state'

ReactDom.render(
	<Provider appState={appState}>
      <BrowserRouter>
        <App/>
      </BrowserRouter>
    </Provider>,
	document.getElementById('root')
)
复制代码

组件中再去使用mobx

/*
 * MobX在Component中的使用
*/
import React from 'react'
import { observer, inject } from 'mobx-react'
import PropTypes from 'prop-types'

import { AppState } from '../../store/app-state'
// 将绑定好的mobx注册到组件中并且observer(监听mobx的state)
@inject('appState') @observer

export default class MobxComponent extends React.Component {
    constructor() {
        super()
        this.changeName = this.changeName.bind(this)
    }
    changeName(event) {
        this.props.appState.changeName(event.target.value)
    }
    render() {
        return (
            <div>
                <div>mobx page</div>
                <input type="text" onChange={this.changeName} />
                <span>{this.props.appState.msg}</span>
            </div>

        )
    }
}

// react传入的props需要声明类型的话,用prop-types插件检测props的类型
MobxComponent.propTypes = {
    appState: PropTypes.instanceOf(AppState)
}
复制代码

完成服务端渲染

当加了react-router和mobx后,需要对服务端渲染做进一步修改,这里不做过多介绍,代码server/文件夹里,提取了development和production环境的公共服务端渲染代码到server/util/server-render.js里。这样react服务端渲染架子搭建完成。

关于SEO配置

用了react-helmet,指定title,meta等内容,然后通过服务端渲染返回内容到ejs模板中

项目运行

运行'development'环境服务端渲染:

npm install
npm run dev:client
npm run dev:server
复制代码

打开http:localhost:3333.

运行'production'环境服务端渲染:

npm install
npm run build
npm run start
复制代码

打开http:localhost:3333.

对于上面的运行命令可以看package.json的'scripts'的启动命令,然后结合服务端渲染代码,更容易理解服务端渲染是怎样配置的。