React最佳實踐嘗試(一)技術選型

Cryptolalia發表於2018-10-16

github地址:github.com/bbwlfx/ts-b…

最近參與了很多遷庫的工作,感覺很多老程式碼已經不太實用,並且存在一些漏洞,加上這點時間聽了很多同事的分享,因此決定嘗試一下構建React的最佳實踐。

為了更好地瞭解react專案結構,鍛鍊自己的能力,這裡並沒有使用create-react-app

目標

  1. SPA + SSR
  2. 首屏資料載入
  3. 優雅的前後端同構
  4. 使用最新的技術棧
  5. ...

技術選型

  1. React + @rematch

    @rematch前段時間新出的redux框架,擁有比redux更簡潔的語法,無需複雜的action creators和thunk middleware。

    github地址:github.com/rematch/rem…

    中文文件地址:rematch.gitbook.io/handbook/ap…

    並且rematch本身支援immer外掛,可以通過mutable的寫法去管理狀態的變化。

    Model定義

    model的定義就是redux的actions、reducer以及state,@rematch將三者合為一個model檔案,每一個modal有三個屬性:state、reducers、effects。

    • state存放當前model的所有狀態
    • reducers存放各種同步修改state的函式,和redux的定義一樣
    • effects存放各種非同步函式,並且不需要任何middleware,@rematch本身就支援async/await寫法。

    並且在effects中,我們可以通過dispatch去呼叫其他model的方法,去修改其他模組的資料。

    // effects的兩種寫法
    dispatch({ type: 'count/incrementAsync', payload: 1 }) // state = { count: 3 } after delay
    dispatch.count.incrementAsync(1)
    複製程式碼
    Modal.js
    export const count = {
        state: 0, // initial state
        reducers: {
            // handle state changes with pure functions
            increment(state, payload) {
            return state + payload
        }
    },
     effects: (dispatch) => ({
        // handle state changes with impure functions.
        // use async/await for async actions
        async incrementAsync(payload, rootState) {
            await new Promise(resolve => setTimeout(resolve, 1000))
                dispatch.count.increment(payload)
            }
        })
    }
    複製程式碼
    Immer外掛
    const todo = {
        state: [{
            todo: "Learn typescript",
            done: true
        }, {
            todo: "Try immer",
            done: false
        }],
        reducers: {
            done(state) {
                state.push({todo: "Tweet about it"})
                state[1].done = true
                return state
            }
        }
    };
    複製程式碼
  2. TypeScript

    選擇ts的最根本的願意其實是因為js已經用爛了,打算嘗試一下ts的使用,因為在網上也看到了很多關於ts優勢的介紹。本著追求極致的原則選擇使用了ts。

  3. Koa@2

    Koa本身是一個十分輕量的node框架,並且擁有豐富的第三方外掛庫以及生態環境,並且Koa本身的易擴充套件性讓我們可以靈活開發,koa2支援的async/await語法也讓非同步請求寫起來十分舒服。

  4. Webpack@4

  5. react-router@4

    本身在前端路由方面選擇了@reach/router,但是使用了一段時間之後發現經常會出現頁面重新整理之後突然滾動到另外的位置上,後來查資料發現@reach/router原始碼中使用了大量的游標操作,據說是為了對殘疾人友好。這些游標操作不知道什麼時候就會產生一些奇怪的bug,因此最終還是放棄了@reach/router選擇了react-router。

    @reach/router和react-router的區別在於:@reach/router是分型路由,支援我們以碎片化的方式定義區域性路由,不必像react-router一樣需要有一個大的路由配置檔案,所有的路由都寫在一起。這種分型路由在大型應用裡面開發起來比較方便,但是同樣也會產生不易維護的副作用。

  6. pug

    模板引擎選擇了pug(jade),pug模板本身使用的是js語法,對前端開發人員十分友好,並且pug本身也支援非常多的功能。koa-pug中介軟體也支援pug引擎。

    doctype html
    
    html
        head
            meta(http-equiv="X-UA-Compatible" content="IE=edge,chrome=1")
            meta(charset="utf-8")
            include ./common_state.pug
            block links
            | !{ styles }
            block common_title
                title TodoList 
            include counter.pug
            block custom_state
        body
            block doms
            #root !{ html }
            | !{ scripts }
    複製程式碼
  7. react-loadable

目錄結構

目錄整體分為前端目錄:public 、 後端目錄:src

public的js目錄中存放檔案如下:

  • components 元件程式碼
  • constants 常量程式碼
  • containers 頁面程式碼
  • decorators 各種裝飾器的程式碼
  • entry 頁面入口程式碼
  • lib 工具庫程式碼
  • models @rematch的Modal程式碼
  • scripts 輔助指令碼程式碼
  • typings ts型別宣告程式碼

src的目錄如下:

  • config 配置檔案
  • controllers 路由處理檔案
  • routes 路由宣告檔案
  • template 模板檔案
  • utils 工具程式碼
  • app.js 後端啟動入口,主要存放運維程式碼
  • server.js server啟動程式碼,主要存放業務程式碼

webpack配置檔案

webpack配置這裡配合webpack-merge,做到webpack配置檔案的拆分。

  • webpack.base.config.js
  • webpack.client.config.js
  • webpack.dev.config.js
  • webpack.prod.config.js
  • webpack.ssr.config.js

base負責基本的配置

client負責前端打包的配置

ssr負責服務端渲染的打包的配置

dev負責開發模式的配置

prod負責生產模式的配置

具體的配置可以到專案原始碼中檢視

其他配置檔案

public和src目錄都需要一個單獨的.babelrc檔案,由於babel7支援通過js的寫法書寫配置檔案了,所以這裡直接用兩個.babelrc.js檔案即可。

public/.babelrc.js
module.exports = api => {
  const env = api.env();
  // 服務端渲染時不載入css
  const importConfig =
    env === "client"
      ? {
          libraryName: "antd",
          libraryDirectory: "es",
          style: true
        }
      : {
          libraryName: "antd"
        };

  return {
    presets: [
      [
        "@babel/env",
        {
          modules: env === "ssr" ? false : "commonjs",
          targets: {
            browsers: ["last 2 versions"]
          }
        }
      ],
      "@babel/react",
      "@babel/typescript"
    ],
    plugins: [
      ["import", importConfig],
      "dynamic-import-node",
      "@babel/plugin-proposal-class-properties",
      [
        "babel-plugin-module-resolver",
        {
          cwd: "babelrc",
          extensions: [".ts", ".tsx"],
          root: ["./"],
          alias: {
            components: "./js/components",
            containers: "./js/containers",
            models: "./js/models",
            decorators: "./js/decorators",
            constants: "./js/constants",
            lib: "./js/lib",
            typings: "./js/typings"
          }
        }
      ],
      "react-loadable/babel"
    ]
  };
};

複製程式碼

babel-plugin-import外掛負責處理對antd的按需載入問題,並且處理ssr不載入css的邏輯。

dynamic-import-node外掛負責處理服務端渲染時候對前端元件動態載入的處理。

module-resolver外掛負責處理alias問題,由於webpack的alias只能在前端使用,服務端渲染的時候無法處理webpack中定義的alias,因此這裡使用外掛來解決這個問題。

src/.babelrc.js
module.exports = {
  presets: [
    [
      "@babel/env",
      {
        targets: {
          node: "current"
        }
      }
    ],
    "@babel/react",
    "@babel/typescript"
  ],
  plugins: [
    "@babel/plugin-proposal-class-properties",
    "dynamic-import-node",
    [
      "babel-plugin-module-resolver",
      {
        cwd: "babelrc",
        alias: {
          components: "../public/js/components",
          containers: "../public/js/containers",
          models: "../public/js/models",
          controllers: "./controllers",
          decorators: "../public/js/decorators",
          server: "./public/buildServer",
          lib: "../public/js/lib",
          typings: "./js/typings"
        },
        extensions: [".ts", ".tsx", ".js", ".jsx"]
      }
    ]
  ]
};
複製程式碼

為了配合SSR,node層的.babelrc檔案也需要同樣一套alias。

tsconfig.json
{
  "compilerOptions": {
    "sourceMap": true,
    "outDir": "./dist/",
    "moduleResolution": "node",
    "jsx": "preserve",
    "module": "esNext",
    "target": "es2015",
    "allowSyntheticDefaultImports": true,
    "allowJs": true,
    "lib": ["es2017", "dom"],
    "baseUrl": ".",
    "noEmit": true,
    "pretty": true,
    "skipLibCheck": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "resolveJsonModule": true,
    "noImplicitReturns": true
  },
  "include": ["**/*.ts", "**/*.tsx"],
  "exclude": ["node_modules", "**/*.spec.ts", "**/*.d.ts"]
}
複製程式碼

其他的還有一切開發的配置檔案,比如.eslintrc,.stylelintrc等看個人喜好配置即可。

為了更好的格式化程式碼,以及在commit之前做一些校驗工作,專案裡新增了husky、lint-staged、prettier-eslint等npm包。 並且在package.json檔案中定義好對應的程式碼:

package.json
"scripts": {
    "precommit": "lint-staged",
    "format": "prettier-eslint --write public/**/*.{js,ts}"
  },
  "lint-staged": {
    "*.{ts,tsx}": [
      "npm run format --",
      "git add"
    ],
    "*.{js,jsx}": [
      "npm run format --",
      "git add"
    ],
    "*.{css,less,scss}": [
      "npm run format --",
      "stylelint --syntax=less",
      "git add"
    ]
  }
複製程式碼

基本的配置到這裡就結束了,下一章開始正式開發的介紹。

系列文章:

  1. React最佳實踐嘗試(二)
  2. React最佳實踐嘗試(三)

相關文章