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
複製程式碼
執行'production'環境服務端渲染:
npm install
npm run build
npm run start
複製程式碼
對於上面的執行命令可以看package.json的'scripts'的啟動命令,然後結合服務端渲染程式碼,更容易理解服務端渲染是怎樣配置的。