從零開始搭建React全家桶環境

Zewail發表於2018-01-31

部落格地址:地址

Github專案地址: 地址

建立專案

 $ mkdir example
 $ cd example
 $ npm init -y複製程式碼

建立目錄結構

├── config              // webpack配置檔案目錄
├── package.json        
├── server              // 開啟本地node服務檔案,包括熱更新等
└── src                 // 原始檔目錄複製程式碼

安裝Webpack

$ npm i --save-dev webpack複製程式碼

編寫Webpack配置檔案

在config目錄下新建檔案:

$ cd config
$ touch webpack.config.dev.js
$ touch webpack.config.pro.js複製程式碼

我們先來寫開發環境下的 webpack.config.dev.js

const path = require('path')
const webpack = require('webpack')
​
// 先定義一些路徑
// 配置資料夾路徑
const CONFIG_PATH = path.resolve(__dirname)
// 原始碼資料夾路徑
const APP_PATH = path.resolve(CONFIG_PATH, '../src')
// 應用入口檔案
const APP_FILE = path.resolve(APP_PATH, 'index.js')
// 打包目錄資料夾路徑
const BUILD_PATH = path.resolve(ROOT_PATH, '../dist')
​
module.exports = {
  // 入口
  entry: APP_FILE,
  // 輸出
  output: {
    // 告訴Webpack結果儲存在哪裡
    path: BUILD_PATH,
    // 打包後的檔名
    filename: 'bundle.js',
    //模板、樣式、指令碼、圖片等資源對應的server上的路徑
    publicPath: "/assets/",
  }
}複製程式碼

一個最簡單的webpack配置檔案已完成

安裝Babel

我們使用ES6來編寫程式碼,所以需要安裝ES6的babel-preset

$ npm i --save-dev babel-cli babel-loader babel-preset-es2015 babel-preset-react babel-preset-stage-0複製程式碼

同時修改webpack配置檔案,在module.exports增加:

module: {
  rules: [{
    test: /\.js$/,
    exclude: /node_modules/,
    loader: "babel-loader"
  }]
}複製程式碼

在根目錄新增babelrc檔案

cd /path/to/example
touch .babelrc複製程式碼

修改.babelrc

{
  "env": {
    "development": {
      "presets" : ["es2015", "stage-0", "react"],
    },
    "production": {
      "presets" : [["es2015", { "modules": false, "loose": true }], "stage-0", "react"],]
    }
  }
}複製程式碼

安裝其他Loader

  • css-loader

  • style-loader

  • postcss-loader

  • less-loader

  • file-loader

  • url-loader

  • autoprefixer

npm i --save-dev css-loader style-loader file-loader less less-loader postcss-loader url-loader autoprefixer複製程式碼

同時修改webpack配置檔案,修改module.exports下的module.rules:

rules: [
  {
    test: /\.js$/,
    exclude: /node_modules/,
    loader: "babel-loader"
  },
  {
    test: /\.css$/,
    use: [
      'style-loader',
      'css-loader',
      {
        loader: 'postcss-loader',
        options: {
          plugins: (loader) => [
            require('autoprefixer')()
          ]
        }
      }
    ]
  },
  {
    test: /\.less$/,
    use: [
      'style-loader',
      'css-loader',
      {
        loader: 'postcss-loader',
        options: {
          plugins: (loader) => [
            require('autoprefixer')()
          ]
        }
      },
      'less-loader'
    ]
  },
  {
    test: /\.(eot|woff|ttf|woff2|svg|gif)(\?|$)/,
    loader: 'file-loader?name=[hash].[ext]'
  },
  {
    test: /\.(png|jpg)$/,
    loader: 'url-loader?limit=1200&name=[hash].[ext]'
  }
]複製程式碼

本地node服務

在server下新建server.js和index.html:

$ cd server
$ touch server.js
$ touch index.html複製程式碼

安裝server服務依賴

$ npm i --save express複製程式碼

修改server.js檔案

const app = new (require('express'))()
// 本地預覽程式碼的埠
const port = 3003
​
app.get('*', function(req, res) {
  res.sendFile(__dirname + '/index.html')
})
​
app.listen(port, function(error) {
  if (error) {
    /*eslint no-console: 0*/
    console.error(error)
  } else {
    /*eslint no-console: 0*/
    console.info("==> ? Listening on port %s. Open up http://localhost:%s/ in your browser.", port, port)
  }
})複製程式碼

最簡單的本地預覽服務已經完成

熱更新

我們使用react-hot-loader來做熱更新,webpack-dev-middlewarewebpack-hot-middlewaref來實時重新整理頁面

安裝依賴

$ npm i --save-dev react-hot-loader webpack-dev-middleware webpack-hot-middleware複製程式碼

編寫index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, user-scalable=no, viewport-fit=contain"/>
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title></title>
</head>
<body ontouchstart="">
  <div id="app"></div>
  <script src="/assets/bundle.js"></script>
</body>
</html>
​複製程式碼

修改server.js

const webpack = require('webpack')
const webpackDevMiddleware = require('webpack-dev-middleware')
const webpackHotMiddleware = require('webpack-hot-middleware')
const config = require('../config/webpack.config.dev.js')
const app = new (require('express'))()
// 本地預覽程式碼的埠
const port = 3003
​
const compiler = webpack(config)
​
app.use(webpackDevMiddleware(compiler, {
  noInfo: true,
  publicPath: config.output.publicPath,
  stats: {
    colors: true
  }
}))
​
app.use(webpackHotMiddleware(compiler))
​
app.get('*', function(req, res) {
  res.sendFile(__dirname + '/index.html')
})
​
app.listen(port, function(error) {
  if (error) {
    /*eslint no-console: 0*/
    console.error(error)
  } else {
    /*eslint no-console: 0*/
    console.info("==> ?  Listening on port %s. Open up http://localhost:%s/ in your browser.", port, port)
  }
})複製程式碼

修改webpack.config.dev.js的module.exports 的entry

entry: [
    'react-hot-loader/patch',
    // 這裡reload=true的意思是,如果碰到不能hot reload的情況,就整頁重新整理。
    'webpack-hot-middleware/client?reload=true',
    APP_FILE
],複製程式碼

在module.exports新增:

plugins: [
    new webpack.DefinePlugin({
      'process.env': {
        NODE_ENV: JSON.stringify('development') //定義編譯環境
      }
    }),
    new webpack.HotModuleReplacementPlugin(),
    new webpack.NoEmitOnErrorsPlugin()
],複製程式碼

在babelrc檔案中新增preset

"plugins" : ["react-hot-loader/babel"]複製程式碼

.babelrc

最終程式碼:

{
  "env": {
    "development": {
      "presets" : ["es2015", "stage-0", "react"],
      "plugins" : ["react-hot-loader/babel"]
    },
    "production": {
      "presets" : [["es2015", { "modules": false, "loose": true }], "stage-0", "react"],
    }
  }
}
​複製程式碼

webpack.config.dev.js

最終程式碼:

/**
 * @author Chan Zewail
 * ###### Thu Jan 25 19:28:40 CST 2018
 */
const path = require('path')
const webpack = require('webpack')
​
// 先定義一些路徑
// 配置資料夾路徑
const CONFIG_PATH = path.resolve(__dirname)
// 原始碼資料夾路徑
const APP_PATH = path.resolve(CONFIG_PATH, '../src')
// 應用入口檔案
const APP_FILE = path.resolve(APP_PATH, 'index.js')
// 打包目錄資料夾路徑
const BUILD_PATH = path.resolve(ROOT_PATH, '../dist')
​
module.exports = {
  entry: [
    'react-hot-loader/patch',
    // 這裡reload=true的意思是,如果碰到不能hot reload的情況,就整頁重新整理。
    'webpack-hot-middleware/client?reload=true',
    APP_FILE
  ],
  output: {
    // 告訴Webpack結果儲存在哪裡
    path: BUILD_PATH,
    // 打包後的檔名
    filename: 'bundle.js',
    //模板、樣式、指令碼、圖片等資源對應的server上的路徑
    publicPath: "/assets/",
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: "babel-loader"
      },
      {
        test: /\.css$/,
        use: [
          'style-loader',
          'css-loader',
          {
            loader: 'postcss-loader',
            options: {
              plugins: (loader) => [
                require('autoprefixer')()
              ]
            }
          }
        ]
      },
      {
        test: /\.less$/,
        use: [
          'style-loader',
          'css-loader',
          {
            loader: 'postcss-loader',
            options: {
              plugins: (loader) => [
                require('autoprefixer')()
              ]
            }
          },
          'less-loader'
        ]
      },
      {
        test: /\.(eot|woff|ttf|woff2|svg|gif)(\?|$)/,
        loader: 'file-loader?name=[hash].[ext]'
      },
      {
        test: /\.(png|jpg)$/,
        loader: 'url-loader?limit=1200&name=[hash].[ext]'
      }
    ]
  },
  plugins: [
    new webpack.DefinePlugin({
      'process.env': {
        NODE_ENV: JSON.stringify('development') //定義編譯環境
      }
    }),
    new webpack.HotModuleReplacementPlugin(),
    new webpack.NoEmitOnErrorsPlugin()
  ],
  resolve: {
    //字尾名自動補全
    extensions: ['.js', '.jsx', '.less', '.scss', '.css'], 
    // 根路徑別名
    alias: {
      '@': `${APP_PATH}/`,
    },
    modules: [
      'node_modules',
      'src',
    ]
  }
}
​複製程式碼

npm指令碼命令

安裝一些工具依賴

npm i --save-dev copyfiles clean-webpack-plugin複製程式碼

在package.json中新增:

"scripts": {
    "dev": "node server/server.js",
    "start": "npm run dev",
    "copy": "copyfiles -f ./server/index.html ./dist",
    "build": "npm run copy && webpack --config config/webpack.config.pro.js"
},複製程式碼

開始編寫專案程式碼

安裝react

npm i --save react react-dom複製程式碼

入口檔案

$ cd src
# 入口檔案
$ touch index.js
# 應用檔案
$ touch App.js複製程式碼

編寫index.js

import React from 'react'
import ReactDOM from 'react-dom'
import { AppContainer } from 'react-hot-loader'
// 應用檔案
import App from './App'
​
ReactDOM.render(
  <AppContainer>
    <App/>
  </AppContainer>
  , document.getElementById('app'))
​
// 熱更新
if (module.hot) {
  module.hot.accept('./App', () => {
    const NextApp = require('./App').default
    ReactDOM.render(
      <AppContainer>
        <NextApp/>
      </AppContainer>,
      document.getElementById('app')
    )
  })
}複製程式碼

全家桶

  • react

  • react-dom

  • react-router

  • redux

  • redux-saga

  • redux-persist

  • history

  • redux-logger

  • react-router-redux

npm i --save react-router redux redux saga reux-persist history redux-logger react-router-redux複製程式碼

編寫configureStore生成store,

$ cd src
$ mkdir store
$ touch index.js複製程式碼
/**
 * @author Chan Zewail
 * ###### Thu Jan 25 19:28:40 CST 2018
 */
import { createStore, applyMiddleware, compose } from 'redux'
import { persistStore } from 'redux-persist'
import reducers from '@/reducers'
import createHistory from 'history/createHashHistory'
import { routerMiddleware } from 'react-router-redux'
import createSagaMiddleware from 'redux-saga'
import sagas from '@/sagas'
import logger from 'redux-logger'
​
// 建立history
export const history = createHistory()
​
//建立saga中介軟體
const sagaMiddleware = createSagaMiddleware()
​
// 需要呼叫的中介軟體
const middleWares = [
  sagaMiddleware,
  routerMiddleware(history),
  logger
]
​
// 生成store
const store = createStore(reducers, undefined, compose(
  applyMiddleware(...middleWares),
))
​
// 將store資料儲存到快取
const persistor = persistStore(store, null)
​
// 生成最終的store函式
export default function configureStore(){
  // 執行saga
  sagaMiddleware.run(sagas)
  return { persistor, store }
}
​
export function getPersistor() {
  return persistor
}
​複製程式碼

編寫reducer

$ cd src
$ mkdir reducers
$ touch index.js複製程式碼
/**
 * @author Chan Zewail
 * ###### Thu Jan 25 19:28:40 CST 2018
 */
import { routerReducer } from 'react-router-redux'
import { persistCombineReducers, persistReducer } from 'redux-persist'
import storage from 'redux-persist/lib/storage'
// import xxxReducer from './xxx'
​
// redux-persist 配置
export const config = {
  key: 'root',
  storage,
  debug: true,
  // 將對應reducer儲存到本地快取的白名單
  whitelist: [
    'routing',
  ]
}
​
​
// 合併reducer
export default persistCombineReducers(config, {
  routing: routerReducer, // 預設引入路由reducer
  // xxx: xxxReducer,
})
​複製程式碼

編寫saga

$ cd src
$ mkdir sagas
$ touch index.js複製程式碼
/**
 * @author Chan Zewail
 * ###### Thu Jan 25 19:28:40 CST 2018
 */
import { fork, all } from 'redux-saga/effects'
// import xxxSagaFlow from './xxxFlow'
​
// 根saga
export default function* rootSaga () {
  yield all([
    // fork(xxxSagaFlow)
  ])
}
​複製程式碼

App.js

/**
 * @author Chan Zewail
 * ###### Thu Jan 25 19:28:40 CST 2018
 */
import React from 'react'
import { Provider } from 'react-redux'
import { history } from '@/store'
import { ConnectedRouter } from 'react-router-redux'
import { PersistGate } from 'redux-persist/es/integration/react'
import { Redirect } from 'react-router-dom'
​
// persister 快取恢復前呼叫的方法
const onBeforeLift = () => {
  // console.log('before action')
}
​
class App extends React.Component {
  constructor(props) {
    super(props)
  }
​
  render() {
    const { persistor, store } = this.props.stores
    // 主頁面
    return (
      <Provider store={store}>
        <PersistGate loading={<div/>} onBeforeLift={onBeforeLift} persistor={persistor}>
          <ConnectedRouter history={history}>
            // switch and route
          </ConnectedRouter>
        </PersistGate>
      </Provider>
    )
  }
}
​
export default App
​複製程式碼

修改 index.js並新增獲取store方法,然後傳遞到App的props(熱更新store)

index.js

/**
 * @author Chan Zewail
 * ###### Thu Jan 25 19:28:40 CST 2018
 */
import React from 'react'
import ReactDOM from 'react-dom'
import { AppContainer } from 'react-hot-loader'
import configureStore from '@/store'
// 主頁面
import App from './App'export const stores = configureStore()
​
ReactDOM.render(
  <AppContainer>
    <App stores={stores}/>
  </AppContainer>
  , document.getElementById('app'))
​
// 熱更新
if (module.hot) {
  module.hot.accept('./App', () => {
    const NextApp = require('./App').default
    ReactDOM.render(
      <AppContainer>
        <NextApp stores={stores}/>
      </AppContainer>,
      document.getElementById('app')
    )
  })
}
​複製程式碼

最後

最基礎的react全家桶已搭建完畢,其他功能可逐漸加入並個性化定製

最終程式碼:github.com/czewail/zew…

使用問題可在Issues提出

歡迎Start


相關文章