博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
React.js 三周 -- 入门到搭建团队基础项目
阅读量:6168 次
发布时间:2019-06-21

本文共 15669 字,大约阅读时间需要 52 分钟。

吐槽

公司自己的产品,由于历史遗留问题,前端一直是和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 = {      width: '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(      
emp

弹框标题

, 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 = {   width: '300px',   height: '165px' } ReactDom.render(     
emp

确认

{msg}

取消
确定
, 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(      
{text}
, 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(    
, 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(        
    {list}
) } else { let levelClass = data.level === 1 ? 'levelTop' : 'levelItem' let margLeft = (data.level * 16) + 'px' menuDom.push(
  • { data.children.length > 0 ?
    { data.level === 1 ?
    icon : '' }
    {data.name}
    { this.state.isShow['item' + data.id] ?
    :
    }
    : data.operate ?
    { data.level === 1 ?
    icon : '' }
    {data.name}
    :
    { data.level === 1 ?
    icon : '' }
    {data.name}
    } {this.handleCreateMenu(data.children, data.id)}
  • ) } return menuDom; } render () { return (
    {this.props.data ? this.handleCreateMenu(this.props.data) : ''}
    ) }}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 的开始,就有小伙伴推荐了 , 作者。这本书称得上是

    良心巨制 。(截至目前,看了四遍)

    转载于:https://www.cnblogs.com/whocare/p/9575620.html

    你可能感兴趣的文章
    类似OutLook布局的开源控件XPanderControls
    查看>>
    Web前端工程师成长之路——知识汇总
    查看>>
    [2018-9-4T2]探索黑暗dark
    查看>>
    【学术信息】中科院2019年学术期刊分区-综合性期刊
    查看>>
    AC自动机模板
    查看>>
    手机缺失sqlite3时操作数据库的多种解决方案 ----adb命令科普
    查看>>
    python2.7_1.3_获取远程设备的IP地址
    查看>>
    数据分析MySQL阶段测验简答题
    查看>>
    如何使用Xcode进行高保真原型设计?
    查看>>
    [转载]Java抽象类和接口的学习
    查看>>
    hdu2087(剪花布条)
    查看>>
    Maven快速入门
    查看>>
    AC自动机
    查看>>
    python多线程抓取代理服务器
    查看>>
    STextComboBox & SComboBox
    查看>>
    「面试题」如何实现一个圣杯布局?
    查看>>
    Ruby的require相关知识
    查看>>
    转:线程引入 pthread_self 解决不同平台的问题!
    查看>>
    核心动画
    查看>>
    执行yiic webapp命令时报错:php.exe不是内部或外部命令,也不是可运行的程序
    查看>>