接了個新項目

背景

最近接了個新項目, 遇到一些問題, 在這整理分享下。

前期規劃

需求是這樣的,須要作一套後臺管理系統: 一個主系統,一個子系統,開發時間6個周。 前期開發有兩我的, 再加一我的。javascript

說實話時間有點緊, 因此前期作好規劃就很重要。 實現先作一個規劃,技術選型,文檔分析,分頁面, 有個大體的評估。css

技術選型

首先肯定的仍是 React 這一套, 即: ReactReduxTypeScript, 樣式管理 styled-components國際化 react-intl, 組件庫 antd, 腳手架,本身配。 原本想圖省事用 CRA(create-react-app),後來以爲用rewired 重寫不太靈活, 並且有個小夥伴也想本身配,熟悉下 webpack , 仍是決定本身搭, 後面會把配置貼出來。html

開發計劃

和後端負責人討論以後決定把這一期的開發任務分紅三個小階段: P1, P2, P3java

P1 完成以後發佈, 先跑通主流程,P2 P3 繼續迭代功能。 node

P1 主要包括:react

  • 開發環境搭建
  • test環境資源申請
  • Nginx 配置
  • 主系統功能開發webpack

    • 三個功能模塊開發
    • 登錄註冊流程
  • 子系統兩個模塊的開發

開發時間: 兩週nginx

壓力仍是有的,時間緊,任務重,並且是第一次帶人作項目, 好在心裏猶如一條老狗,一點都不慌。web

後面就進入了開發階段, 遇到了挺多問題。json

進入開發

搭建開發環境

這一步你們就都很熟悉了,添加各類配置和打包。 由於主系統和子系統頁面風格都是同樣的, 不必分紅兩個系統, 把新開一個文件夾,裏面放子系統的頁面, 而後打成不一樣的包就能夠了。就有了以下配置:

// webpack.config.js

const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const webpack = require('webpack')
const fs = require('fs')
const lessToJS = require('less-vars-to-js')

const { NODE_ENV } = process.env

const isAdminApp = process.env.APP_TYPE === 'admin'
const getBaseurl = () => {
  switch (process.env.ENV) {
    case 'id':
      return 'https://xxx.test.shopee.co.id'
    default:
      return ''
  }
}

const plugins = [
  new HtmlWebpackPlugin({
    template: path.resolve(__dirname, 'template.html'),
    title: isAdminApp ? 'WMS LITE ADMIN' : 'WMS LITE',
  }),
  new webpack.DefinePlugin({
    __BASEURL__: JSON.stringify(getBaseurl()),
  }),
  new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/)
]
if (NODE_ENV !== 'production') {
  plugins.push(new webpack.SourceMapDevToolPlugin({}))
}

const themeVariables = lessToJS(fs.readFileSync(path.resolve(__dirname, './assets/antd-custom.less'), 'utf8'))

const port = isAdminApp ? 9527 : 8080

module.exports = {
  entry: [
    '@babel/polyfill',
    isAdminApp ? './admin/index.js' : './pages/index.js'
  ],
  output: {
    filename: isAdminApp ? 'admin.[hash:8].js' : 'main.[hash:8].js',
    path: path.resolve(__dirname, isAdminApp ? 'dist/adminstatic' : 'dist/static'),
    publicPath: isAdminApp ? '/admin/' : '/',
  },
  mode: NODE_ENV,
  devtool: false,
  plugins,
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
        },
      },
      {
        test: /\.less$/,
        use: [
          { loader: 'style-loader', },
          { loader: 'css-loader', },
          {
            loader: 'less-loader',
            options: {
              javascriptEnabled: true,
              sourceMap: true,
              modifyVars: themeVariables,
            },
          }
        ],
      },
      {
        test: /\.css$/,
        use: [
          { loader: 'style-loader', },
          { loader: 'css-loader', }
        ],
      },
      {
        type: 'javascript/auto',
        test: /\.mjs$/,
        use: [],
      },
      {
        test: /\.(png|jpg|gif|svg)$/i,
        use: [
          {
            loader: 'url-loader',
            options: {
              limit: 8192,
            },
          }
        ],
      }
    ],
  },
  optimization: {
    runtimeChunk: {
      name: 'manifest',
    },
    splitChunks: {
      chunks: 'all',
      minSize: 30000,
      minChunks: 1,
      maxAsyncRequests: 5,
      maxInitialRequests: 3,
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules/,
          filename: 'vendor.[chunkhash:8].js',
          enforce: true,
          priority: 5,
        },
        antd: {
          test: /[\\/]node_modules[\\/]antd[\\/]/,
          filename: 'antd.[chunkhash:8].js',
          priority: 10,
        },
        antdIcons: {
          test: /[\\/]node_modules[\\/]@ant-design[\\/]/,
          filename: 'antd-icons.[chunkhash:8].js',
          priority: 15,
        },
        styles: {
          test: /\.(scss|css)$/,
          filename: 'styles.[hash:8].css',
          minChunks: 1,
          reuseExistingChunk: true,
          enforce: true,
          priority: 20,
        },
      },
    },
  },
  devServer: {
    historyApiFallback: isAdminApp ? {
      rewrites: [{ from: /.*/g, to: '/admin/index.html', }],
    } : true,
    hot: true,
    port,
    proxy: [{
      context: ['/admin/api', '/api'],
      target: 'https://gudangku.test.shopee.co.id',
      changeOrigin: true,
      onProxyRes(proxyRes, _, res) {
        const cookies = proxyRes.headers['set-cookie'] || []
        const re = /domain=[\w.]+;/i
        const newCookie = cookies.map(cookie => cookie.replace(re, 'Domain=localhost;'))
        res.writeHead(200, {
          ...proxyRes.headers,
          'set-cookie': newCookie,
        })
      },
    }],
  },
}
// package.json

  "scripts": {
    "start": "NODE_ENV=development APP_TYPE=main webpack-dev-server",
    "build": "NODE_ENV=production APP_TYPE=main webpack",
    "start:admin": "NODE_ENV=development APP_TYPE=admin webpack-dev-server",
    "build:admin": "NODE_ENV=production APP_TYPE=admin webpack",
    "lint": "eslint ./ --ext js",
    "i18n": "node i18n/index.js"
  },

根據不一樣的參數打包, 主系統打包到 dist/static, 子系統打包到dist/adminstatic.

解決完打包的問題, 還有另外一個問題, 就是本地開發的時候須要配置代理。

目前比較通用的作法有:

  1. devServer 配置 proxy
  2. 修改 host
  3. Nginx 作反向代理

// 也能夠說只有兩種。

我用的是1, 緣由是比較靈活, 這個系統後面要發佈到7個或者更多的國家, 改host 總歸是不太優雅, 來回倒騰Nginx 又費時費力, 提個單大半天不批,不太方便。

後面又遇到的問題是登錄的時候須要請求一次csrftoken, 由於 domain 不匹配因此cookie 種不進來, 因此就改了下配置,代碼見 devServer 部分,這個問題就解決了。

打包優化

初步作了個優化, 代碼分包, 這個系統antd 用的比較多,代碼體積, 和業務代碼打在一個包裏明顯是不合適的,就簡單分了一下:

clipboard.png

壓縮後整體積900K。

clipboard.png

FCP 1s, 勉強還能接受, 後面有須要再作優化。

國際化實現

國際化用的是`react-intl`, 用起來就很簡單了:

主要就兩種形式:
  1. 直接翻譯:

    <FormattedMessage id="xxx" />

  2. 須要特殊傳, 好比 placeholder, Modal 的title等,若是直接用1 的方式會顯示一個[object Object] ,好在react-intl 提供了 injectIntl 方法能夠解決這個問題:
<FormattedMessage /> is a component which cannot be placed to placeholder which expects a raw String.

用法:

import {injectIntl} from 'react-intl'; 

class TestComponent extends React.Component{
  render(){
    const { intl } = this.props;
    return (
        <input placeholder={intl.formatMessage({ id: "loginPage.username", defaultMessage: 'username'})}/>
    )
  }
}

export default injectIntl(TestComponent);

傳入的 id, 是你本身定義的,若是有翻譯平臺的話, 能夠本身添加這些key:

clipboard.png

在翻譯平臺完成翻譯後, 須要下載到本地, 須要手動下載, 感受很麻煩, 因而我就寫了個腳原本自動下載, 翻譯平臺更新後, 執行下 yarn i18n 就能夠更新過來了:

clipboard.png

頁面字段的替換就按上面的兩種方法, 純粹的體力活, 沒什麼好說的。

Nginx 配置

功能開發完以後, 要發佈到測試環境, 中間要配置Nginx, 我這有個配置平臺, 加配置以後提單, 自動部署。

配置的時候仍是遇到一些問題的。

首先解決 index.html 訪問路徑的問題:

須要配置的路徑有:

  1. /
  2. /index.html
  3. /admin
  4. /admin/index.html

首先看 //index.html

clipboard.png

clipboard.png

還須要配置環境和地區:

clipboard.png

/admin/admin/index.html 也同樣的配置。

不過須要注意的是, //admin 須要配置 try_files :

/ :

clipboard.png

/admin :

clipboard.png

對應生成的 conf 文件:

clipboard.png

什麼是try_files

從字面上理解就是嘗試文件,再結合環境理解就是嘗試讀取文件, 那是想讀取什麼文件呢,讀取 靜態文件.

$uri, 這個是nginx的一個變量,存放着用戶訪問的地址. 好比:http://www.xxx.com/index.html, 那麼$uri就是 /index.html

$uri/ 表明訪問的是一個目錄,好比:http://www.xxx.com/hello/test/, 那麼$uri/就是 /hello/test/

完整的解釋就是:

try_files 去嘗試到網站目錄讀取用戶訪問的文件, 若是第一個變量存在,就直接返回;
不存在繼續讀取第二個變量,若是存在,直接返回;不存在直接跳轉到第三個參數上。

至於爲何要配try_files , 由於咱們的路由是基於browserHistory的, 若是用 hashHistory 就不用配 try_files。 你可能要問, 既然 hashHistory 能夠不用配 try_files, 爲何你還要用browserHistory 呢?

多是由於, 加個/#/ 看起來比較醜吧 :)

未完待續, 持續更新..

  • 5.21 目前處於P3階段, 主要功能開發完成,計劃5.27號上測試,進度上問題不大。 後面有什麼值得分享的問題再說。
相關文章
相關標籤/搜索