zoukankan      html  css  js  c++  java
  • React.js 三周 -- 入门到搭建团队基础项目

    吐槽

    公司自己的产品,由于历史遗留问题,前端一直是和java放到一个项目里写的。
    导致了,前端就被死死的绑在了IDEA战车上。想要看页面效果,先起几个java服务。想要调试一个改动,重启个java服务先。(热更新是有,但是间歇性失效,会给调试带来意想不到的困扰。)

    选择 React.js 的原因

    打算做前后分离,之前的技术路线是 Vue.js 多页。想多掌握些技能,对现有产品的结构,进行一次改革,解放前端。(产品的代码量已经不小了)于是咨询大佬,在方少和各位 React.js 大佬的力荐下。大胆的尝试使用 React.js (以前虽然接触过,但没写过)。如果只是实现逻辑,什么框架都可以。写过之后,React.js 的那种掌控感,可实现性,个人很喜欢,虽然也遇到很多坑。说这些,希望能给遇到类似问题的开发者一点参考。

    正文

    主要记录一下,从不会到折腾出一些东西的过程。写做分享,也写给自己。
    :文中涵盖的内容,可能不是有多难,也可能存在一些不正确性。

    前期

    项目结构

    一个适合的项目结构,会给开发带来极大的快感。
    > 代表文件夹

        ./src
            >assets // 静态资源
                >font-awesome
                logo.png
            >components // 放置 dumb 组件
                >alert
                    >icon // 将 dumb 组件需要的 icon 放到一起,方便管理
                    alert.js
                    alert.less // css 模块化,只针对 alert.js 不会造成命名污染
                ...
            >containers // 放置 smart 组件
            >http
                >api // 针对不同模块的 api ,进行编写,方便多人开发,互不干扰
                http.js // 对 axios 的统一配置
            >utils
                skin.js // 皮肤文件
            App.js
            index.js
            registerServiceWork.js
            router.js // 将路由抽离,方便管理和修改
        config-overrides.js // webpack 配置
        package.json
    

    package.json

    
    ...
    "dependencies": {
        "ajv": "^6.5.2",
        "axios": "^0.18.0",
        "base64-js": "^1.3.0",
        "crypto-js": "^3.1.9-1",
        "file-loader": "^1.1.11",
        "prop-types": "^15.6.2",
        "react": "^16.4.2",
        "react-app-rewire-less": "^2.1.2",
        "react-dom": "^16.4.2",
        "react-modal": "^3.5.1",
        "react-router-dom": "^4.3.1",
        "react-scripts": "1.1.4",
        "react-app-rewired": "^1.5.2"
      },
      ...
      
    
    • 为什么没有 redux react-redux ? 放在后面聊,其中也是有些取舍,有些爱恨情仇。
    • 代码检测?当然 eslint 个人习惯用这个

    webpack 配置

    项目开始,使用 create-react-app,让人意想不到的是,项目开始的困难,并不是来自 React.js 的编写,而是 webpack 4.0create-react-app 中的配置,是隐藏起来的。npm eject 命令可以把配置暴露出来,进行配置。最后发现 react-app-rewired ,让我比较优雅的完成了配置。
    react-app-rewired 的配置全都写在 config-overrides.js 中,放在项目根下,与 ./src 同级,下面是我的配置,及部分解释。
    config-overrides.js

    const path = require('path')
    const rewireLess = require('react-app-rewire-less');
    
    /**
     * @author Itroad
     * @version 0.1.0
     * 
     * Cover webpack's configure
     * @param { object } config webpack export.module = {...}
     * @param { string } env production || development
     * @return { object } custom config
     */
    module.exports = function override(config, env) {
      if (env === "production") {
    
        // File path of build
        // 解决 打包后 文件引用路径问题
        // 也可以在 package.json homepage: '.',配置 但我喜欢放在一起,方便管理
        config.output.publicPath = '.' + config.output.publicPath
    
        // For require source file outside of src/. ( remove ModuleScopePlugin )
        // config.resolve.plugins = []
    
        // For css module
        config.module.rules[1].oneOf[2].loader[2].options['modules'] = true
        config.module.rules[1].oneOf[2].loader[2].options['localIdentName'] = '[name]_[local]__[hash:base64:5]'
    
        // 配置css 内对于 font 字体文件的引用路径
        config.module.rules[1].oneOf[3].options['publicPath'] = '../../'
        
        // Path alias
        config.resolve.alias = Object.assign({}, config.resolve.alias, {
          "@src": path.resolve("src/"),
          "@http": path.resolve("src/http"),
          "@assets": path.resolve("src/assets"),
          "@components": path.resolve("src/components"),
          "@containers": path.resolve("src/containers"),
          "@reducers": path.resolve("src/reducers"),
          "@styles": path.resolve("src/styles"),
          "@utils": path.resolve("src/utils"),
          "@static": path.join(process.cwd(), './static') // 引用./src 外部资源,默认只在./src 内,本文并未使用,这里只做类举,不做推荐
        })
    
      } else {
    
        // For require source file outside of src/. ( remove ModuleScopePlugin )
        // config.resolve.plugins = []
    
        // For css module
        config.module.rules[1].oneOf[2].use[1].options['modules'] = true
        config.module.rules[1].oneOf[2].use[1].options['localIdentName'] = '[name]_[local]__[hash:base64:5]'
        // config.module.rules[1].oneOf[2].exclude = [
        //   path.resolve(__dirname, 'node_modules'),
        //   path.resolve(__dirname, 'src/components'),
        // ]
        config.module.rules[1].oneOf.push({
          test: /.css$/,
          use: ['style-loader', 'css-loader'],
          include: [
            path.resolve(__dirname, 'node_modules'),
            path.resolve(__dirname, 'src/components'),
          ]
        })
        
        // Path alias
        config.resolve.alias = Object.assign({}, config.resolve.alias, {
          "@src": path.resolve("src/"),
          "@http": path.resolve("src/http"),
          "@assets": path.resolve("src/assets"),
          "@components": path.resolve("src/components"),
          "@containers": path.resolve("src/containers"),
          "@reducers": path.resolve("src/reducers"),
          "@styles": path.resolve("src/styles"),
          "@utils": path.resolve("src/utils"),
          "@static": path.join(process.cwd(), './static')
        })
      }
      
      // To support less
      config = rewireLess(config, env);
    
      return config;
    }
    
    • 对打包后,index.html 中的文件路径,以及 .css 文件中对外部资源引用的路径
    • css 模块化,这样命名就不是头疼(之后会具体说明)
    • 路径别名,避免路径写错,看着也优雅。缺点就是,vs code 的路径自动匹配不能用了

    中期

    项目结构,配置都搞定了。开始进入代码的编写。
    毕竟这不是教程,那就说一些,我认为有点价值的。

    由于没有采用任何的 UI 框架,所以都要自己实现

    import React from 'react'
    import ReactDom from 'react-dom'
    import font from  '@assets/font-awesome/css/font-awesome.min.css'
    import style from './modal.less'
    
    const createModal = (Component, imgSrc, ModalStyle) => {
      let body = document.body;
      let showDom = document.createElement("div");
      // 设置基本属性
      showDom.classList.add(style.toast)
      
      body.appendChild(showDom);
    
      // 自我删除的方法
      let close = () => {
          ReactDom.unmountComponentAtNode(showDom);
          body.removeChild(showDom);
      }
    
      if(!ModalStyle) {
        ModalStyle = {
           '400px',
          height: '500px'
        }
      }
      if(ModalStyle) {
        if(parseInt(ModalStyle.width, 10)) {
          ModalStyle.width = parseInt(ModalStyle.width, 10) > (window.innerWidth - 100) ? (window.innerWidth - 100) : ModalStyle.width
        } else {
          ModalStyle.width = '400px'
          console.error('createToast width 属性值输入错误, 已使用默认值')
        }
    
        if(parseInt(ModalStyle.height, 10)) {
          ModalStyle.height = parseInt(ModalStyle.height, 10) > (window.innerHeight - 100) ? (window.innerHeight - 100) : ModalStyle.height
        } else {
          ModalStyle.height = '500px'
          console.error('createToast height 属性值输入错误, 已使用默认值')
        }
      }
    
      ReactDom.render(
          <div className='ReactModal__Content' style={ModalStyle}>
            <div className={style.head + ' skin-modal-head'}>
              <div>
                <img src={imgSrc} alt="emp"/>
                <p>弹框标题</p>
              </div>
              <i className={font['fa'] + ' ' + font['fa-close']} onClick={close}></i>
            </div>
            <Component close={close}/>
          </div>,
          showDom
      );
    }
    
    export default createModal
    
    • 也许这是函数式编程吧
    • 自己创建节点,灵活植入,用完删除
    • 尺寸做了默认的设置,和根据网页可视范围宽高,对超出范围进行的处理
    • 通过 props 将弹窗关闭方法传入子组件
    • 还有一点,说不上多好的告警日志提示
    • 官网也有 createPortal() 可供使用,我是写完之后才知道,也就不改了
    • parseInt(value, 10) 防止 '' ' ' 和 不可转为数字的值

    Alert

    类似的方法,实现了 Alert ,其中包括:createConfirm createInfo createWarning createErrorclearAlert 清除 Alert 的方法。说下 createConfirm 其他的没什么。

    const createConfirm = (msg, cb) => {
     showDom = document.createElement("div");
    
     // 设置基本属性
     showDom.classList.add(style.toast)
     document.body.appendChild(showDom);
    
     // 自我删除的方法
     let close = () => {
         ReactDom.unmountComponentAtNode(showDom);
         document.body.removeChild(showDom);
     }
    
     const ModalStyle = {
        '300px',
       height: '165px'
     }
    
     ReactDom.render(
         <div className='ReactModal__Content' style={ModalStyle}>
           <div className={style.head + ' skin-modal-head'}>
             <div>
               <img src={confirm} alt="emp"/>
               <p>确认</p>
             </div>
             <i className={font['fa'] + ' ' + font['fa-close']} onClick={close}></i>
           </div>
           <div className={style.confirmContent}>
             <p className={style.msg}>
               {msg}
             </p>
           </div>
           <div className={style.footer}>
             <div onClick={close}>取消</div>
             <div onClick={cb}>确定</div>
           </div>
         </div>,
         showDom
     );
    }
    

    调用示例

    
    /**
      * confirm 点击确认按钮的回调
      * @param {any} params 
      */
     confirmCallBack (params) {
       console.log('test', params)
       clearAlert()
     }
    
     /**
      * 测试 confirm 弹窗
      */
     showNewConfirm () {
       createConfirm('确定要删除xxx ?', this.confirmCallBack.bind(this, 123))
     }
    
    
    • 用了一个 cb 回调,来处理 confirm 点击确定的事件

    Toast

    这个就更简单了

    import React from 'react'
    import ReactDom from 'react-dom'
    import style from './toast.less'
    
    const createToast = (text, time) => {
      let body = document.body;
      let showDom = document.createElement("div");
      // 设置基本属性
      showDom.classList.add(style.toast)
      
      body.appendChild(showDom);
    
      // 自我删除的方法
      let close = () => {
          ReactDom.unmountComponentAtNode(showDom);
          body.removeChild(showDom);
      }
      if(!parseInt(time, 10)) {
        time = 1500
      }
      setTimeout(close, time)
      ReactDom.render(
          <div className={style.content}>
            {text}
          </div>,
          showDom
      );
    }
    
    export default createToast
    
    • 第二个参数,传入关闭时间,默认 1500 毫秒

    皮肤

    这个就是,将皮肤样式,添加在页面的 <style></style>

    /**
     * @author Jiang yang
     * 
     * @description 生成皮肤样式
     * @version 0.0.1
     */
    
    const skin = {}
    
    skin.iceBlue = {
      // 全局字体颜色
      appColor: '#FFFFFF',
      appBgColor: 'black',
    
      // header
      headerBgColor: '#010a1c', // 框架头部背景色
    
      // left menu
      leftMenuBgColor: '#2c3e50', // 左侧菜单背景色
      leftMenuBorderColor: '#2c3e50', // 左侧菜单边颜色
    
      // right menu
      rightMenuBgColor: '#2c3e50', // 右侧菜单背景色
      rightMenuBorderColor: '#2c3e50', // 右侧菜单边颜色
    
      // content
      contentBgColor: 'rgb(60, 71, 84)', // 框架内容部分背景色
    
      // footer
      footerBgColor: '#2c3e50', // 框架底部背景色
      footerShadowColor: 'black', // 框架底部阴影色
    
      // modal
      modalOverlay: 'rgba(49, 52, 70, 0.75)', // 弹窗遮罩层
      modalContentBg: '#1f2c3a', // 弹窗背景
      modalContentShadow: 'gray', // 弹窗阴影
      modalContentTxt: 'white', // 弹窗字体颜色
      modalHeadBg: '#091323' // 弹窗头部
    }
    
    skin.lightBlue = {
      // 全局字体颜色
      appColor: 'black',
      appBgColor: 'white',
    
      // header
      headerBgColor: 'blue',
    
      // footer
      footerBgColor: 'blue',
      footerShadowColor: 'black',
    
      // left menu
      leftMenuBgColor: 'white',
      leftMenuBorderColor: '#2c3e50',
    
      // right menu
      rightMenuBgColor: 'white',
      rightMenuBorderColor: '#2c3e50',
    
      // content
      contentBgColor: 'white',
    }
    
    let getSkinStyle = (skin) => {
      if(!skin) {
        return '';
      }
      return `
        .skin-app {
          color: ${skin.appColor};
          background-color: ${skin.appBgColor};
        }
        .skin-header {
          background-color: ${skin.headerBgColor};
        }
        .skin-left-menu {
          background-color: ${skin.leftMenuBgColor};
          border-right: 1px solid ${skin.leftMenuBorderColor};
        }
        .skin-right-menu {
          background-color: ${skin.rightMenuBgColor};
          border-left: 1px solid ${skin.rightMenuBorderColor};
        }
        .skin-content {
          background-color: ${skin.contentBgColor};
        }
        .skin-footer {
          background-color: ${skin.footerBgColor};
          box-shadow: 0 -1px 10px ${skin.footerShadowColor};
        }
        .ReactModal__Overlay {
          background-color: ${skin.modalOverlay} !important;
        }
        .ReactModal__Content {
          background-color: ${skin.modalContentBg} !important;
          box-shadow: 0px 0px 10px ${skin.modalContentShadow};
          color: ${skin.modalContentTxt};
        }
        .skin-modal-head {
          background-color: ${skin.modalHeadBg};
        }
      `
    }
    
    let setSkinStyle = (skin) => {
      let styleText = getSkinStyle(skin);
      let oldStyle = document.getElementById('skin');
      const style = document.createElement('style');
      style.id = 'skin';
      style.type = 'text/css';
      style.innerHTML = styleText;
      oldStyle ? document.head.replaceChild(style, oldStyle) : document.head.appendChild(style);
    }
    
    setSkinStyle(skin.iceBlue)
    
    export {skin, setSkinStyle}
    
    

    index.js

    import React from 'react';
    import ReactDOM from 'react-dom';
    import { HashRouter, Route } from 'react-router-dom'
    import App from './App';
    import Login from './components/login/login.js'
    import registerServiceWorker from './registerServiceWorker';
    
    import '@utils/skin' // 这里引入即可
    
    ReactDOM.render(
        <HashRouter>
          <div style={{height:'100%'}}>
            <Route exact path='/' component={Login} />
            <Route path='/home' component={App} />
          </div>
        </HashRouter>,
      document.getElementById('root')
    );
    registerServiceWorker();
    
    
    • 定义皮肤样色对象,然后根据对象生成样式,然后插入到页面中
    • 多个皮肤,就多个皮肤颜色对象

    其实这本来也不是什么有价值的东西。但是用 React 实现的,对于没做过类似的,算是个参考吧。
    menu.js

    import React, { Component } from 'react'
    import PropTypes from 'prop-types'
    import { Link } from 'react-router-dom'
    import font from  '@assets/font-awesome/css/font-awesome.min.css'
    import style from './menu.less'
    
    class Menu extends Component {
      static propTypes = {
        data: PropTypes.array
      }
    
      constructor () {
        super()
        this.state = {
          isShow: null
        }
    
      }
    
      /**
       * 生命周期钩子: 根据 props 的变化,更新 state
       * @param {object} nextProps 
       */
      static getDerivedStateFromProps (nextProps, prevState) {
        if (prevState.isShow) {
          return null
        }
        
        const data = nextProps.data
    
        /**
         * 递归生成 isShow 对象,控制菜单的展开和收缩
         * @param {array | object} data 
         * @return {item+id: true, item2: false, ...}
         */
        function getIsShow (data) {
          let isShow = {}
          function getIsShowState (data) {
            if(data instanceof Array) {
              for(let item of data){
                getIsShowState(item)
              }
            } else {
              isShow['item' + data.id] = data.show
              getIsShowState(data.children)
            }
          }
          getIsShowState(data)
          return isShow
        }
    
        return {
          isShow: data ? getIsShow(data) : null
        }
        
      }
    
      /**
       * 通过 id 来查找 this.state.isShow 中的数据,从而控制菜单的显示状态
       * @param {number} id 菜单 id
       */
      handleClickMenu (id) {
        let current = {
          ['item'+ id]: !this.state.isShow['item'+ id]
        }
        let copyState = this.state.isShow
        this.setState({
          isShow: Object.assign({}, copyState, current)
        })
      }
    
      handleDisposeOperate (value) {
        if(this.props.operateCallBack) {
          this.props.operateCallBack(value)
        }
      }
    
      /**
       * 递归生成菜单的 DOM 结构
       * @param {array} data 菜单数据
       * @param {number} id 菜单id
       */
      handleCreateMenu (data, id) {
        let menuDom = [];
    
        if (data instanceof Array) {
          let list = []
          for (let item of data) {
            list.push(this.handleCreateMenu(item))
          }
          menuDom.push(
            <ul key={id ? 'ul-' + id : 'root'} className={style.menuUl} style={{display: id ? this.state.isShow['item' + id] ? 'block' : 'none' : 'block'}}>
              {list}
            </ul>
          )
        } else {
          let levelClass = data.level === 1 ? 'levelTop' : 'levelItem'
          let margLeft = (data.level * 16) + 'px'
    
          menuDom.push(
            <li key={data.id} id={data.id}>
              {
                data.children.length > 0
                ? <div onClick={this.handleClickMenu.bind(this, data.id)} className={style[levelClass]} style={{'paddingLeft': margLeft}}>
                    <div>
                      {
                        data.level === 1
                        ? <img className={style.icon} src={require('./icon/' + data.icon)} alt="icon"/>
                        : ''
                      }
                      <span>{data.name}</span>
                    </div>
                    {
                      this.state.isShow['item' + data.id]
                      ? <i className={font['fa'] + ' ' + font['fa-angle-down']}></i>
                      : <i className={font['fa'] + ' ' + font['fa-angle-right']}></i>
                    }
                  </div>
                :
                  data.operate
                  ? <div onClick={this.handleDisposeOperate.bind(this, data.operate)}>
                      <div className={style[levelClass]} style={{'paddingLeft': margLeft}}>
                        <div>
                          {
                            data.level === 1
                            ? <img className={style.icon} src={require('./icon/' + data.icon)} alt="icon"/>
                            : ''
                          }
                          <span>{data.name}</span>
                        </div>
                      </div>
                    </div>
                  : <Link to={data.path}>
                      <div className={style[levelClass]} style={{'paddingLeft': margLeft}}>
                        <div>
                          {
                            data.level === 1
                            ? <img className={style.icon} src={require('./icon/' + data.icon)} alt="icon"/>
                            : ''
                          }
                          <span>{data.name}</span>
                        </div>
                      </div>
                    </Link>
              }
              
              {this.handleCreateMenu(data.children, data.id)}
                
            </li>
          )
        }
    
        return menuDom;
      }
    
      render () {
        return (
          <div className=''>
            {this.props.data ? this.handleCreateMenu(this.props.data) : ''}
          </div>
        )
      }
    }
    
    export default Menu
    
    • handleCreateMenu() 根据数据,生成菜单结构
    • 关于这种结构,官网也提及,要有 key 这就是为什么传入了 id,同时也是为了控制菜单折叠
    • 菜单的折叠,用 state 来控制,具体看 getIsShow()
    • 这里面我用了些 三元判断 甚至还嵌套了下

    部分生命周期将要废弃

    虽说这个要看官网,但是在看 React 小书 的时候,也因为生命周期的问题,耽搁了一下。

    • 首先提供一下官网说明,中文的
    • componentWillMount() 这个就不用了,挂载之前的逻辑,写到 constructor()
    • componentWillUpdate() componentWillReceiveProps() 这两个不用了,写到 static getDerivedStateFromProps()

    后期

    发现前面该开发的,都写差不多。后期也没什么了。

    • 我写了两份文档,DOCS.mdSTANDARD.md
      DOCS.md 是关于相关插件,函数的使用和说明
      STANDARD.md 是这个项目的代码规范文档,部分针对这个项目,多数都是普遍的规范

    • 要求打包之后,还要灵活的配置 接口公共路径
      public 中添加一个文件,env.js ,然后在 index.html 中手动引入
      env.js

      window.PROJECT_CONFIG = {
      production: "http://..."
      }
      

      就是把变量放到 window 下,这样代码里就能 window.PROJECT_CONFIG.production 取到想要的数据,打包后,这个文件也一样存在,可以更改。环境变更的时候,就不用改代码,然后再打包了。

    为何没有使用 Redux ?

    这是部分页面
    部分界面
    图中两个组件,是有频繁交互的。当时是考虑用 reduxreact-redux 来写。而我也实现了。 但是后来思前想后,觉得这种方式,如果让组里其他前端开发的话,不是那么友好。会写很多 map...
    于是,我把右侧菜单,嵌套进了中间的组件中,外面看着没变化。但通信方式,已经变成了父子组件之间的通信方式。这就比较简单了。
    这种处理方式,我认为是,用结构,来取代复杂的逻辑。
    是的,每个页面,如何需要,都要带有自己的右侧菜单,仁者见仁智者见智吧。

    特别感谢

    这个一定要说在前面,在学习 React.js 的开始,就有小伙伴推荐了 react 小书, 作者胡子大哈。这本书称得上是
    良心巨制 。(截至目前,看了四遍)

  • 相关阅读:
    shell script数组使用函数输出
    Yii2文件上传
    ubuntu 安装遇到黑屏
    使用函数
    ionCube 安装
    记录LNMP环境彻底删除绑定域名及网站文件夹/文件的过程
    lnmp环境 开启pathinfo
    国外知名设计教程网址收集
    26个国外在线教育网站
    前端学习网站汇总
  • 原文地址:https://www.cnblogs.com/whocare/p/9575620.html
Copyright © 2011-2022 走看看