React專案架構,掌握前端架構師的核心本領

童話鎮發表於2018-08-20

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.

React專案架構,掌握前端架構師的核心本領

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'的啟動命令,然後結合服務端渲染程式碼,更容易理解服務端渲染是怎樣配置的。

相關文章