4

背景


最近接了个新项目, 遇到一些问题, 在这整理分享下。

前期规划

需求是这样的,需要做一套后台管理系统: 一个主系统,一个子系统,开发时间6个周。 前期开发有两个人, 再加一个人。

说实话时间有点紧, 所以前期做好规划就很重要。 实现先做一个规划,技术选型,文档分析,分页面, 有个大致的评估。

技术选型

首先确定的还是 React 这一套, 即: ReactReduxTypeScript, 样式管理 styled-components国际化 react-intl, 组件库 antd, 脚手架,自己配。 本来想图省事用 CRA(create-react-app),后来觉得用rewired 重写不太灵活, 而且有个小伙伴也想自己配,熟悉下 webpack , 还是决定自己搭, 后面会把配置贴出来。

开发计划

和后端负责人讨论之后决定把这一期的开发任务分成三个小阶段: P1, P2, P3

P1 完成之后发布, 先跑通主流程,P2 P3 继续迭代功能。

P1 主要包括:

  • 开发环境搭建
  • test环境资源申请
  • Nginx 配置
  • 主系统功能开发

    • 三个功能模块开发
    • 登陆注册流程
  • 子系统两个模块的开发

开发时间: 两周

压力还是有的,时间紧,任务重,而且是第一次带人做项目, 好在内心犹如一条老狗,一点都不慌。

后面就进入了开发阶段, 遇到了挺多问题。

进入开发

搭建开发环境

这一步大家就都很熟悉了,添加各种配置和打包。 因为主系统和子系统页面风格都是一样的, 没必要分成两个系统, 把新开一个文件夹,里面放子系统的页面, 然后打成不同的包就可以了。就有了如下配置:

// 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号上测试,进度上问题不大。 后面有什么值得分享的问题再说。

皮小蛋
8k 声望12.8k 粉丝

积跬步,至千里。