whocare
## 吐槽 公司自己的产品,由于历史遗留问题,前端一直是和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.0` 。`create-react-app` 中的配置,是隐藏起来的。`npm eject` 命令可以把配置暴露出来,进行配置。最后发现 `react-app-rewired` ,让我比较优雅的完成了配置。 `react-app-rewired` 的配置全都写在 `config-overrides.js` 中,放在项目根下,与 `./src` 同级,下面是我的配置,及部分解释。 config-overrides.js ``` javascript 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 框架,所以都要自己实现 #### Modal ``` javascript 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` `createError` 和 `clearAlert` 清除 `Alert` 的方法。说下 `createConfirm` 其他的没什么。 ``` javascript 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 ); } ``` 调用示例 ``` javascript /** * confirm 点击确认按钮的回调 * @param {any} params */ confirmCallBack (params) { console.log('test', params) clearAlert() } /** * 测试 confirm 弹窗 */ showNewConfirm () { createConfirm('确定要删除xxx ?', this.confirmCallBack.bind(this, 123)) } ``` * 用了一个 `cb` 回调,来处理 `confirm` 点击确定的事件 #### Toast 这个就更简单了 ``` javascript 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 毫秒 #### 皮肤 这个就是,将皮肤样式,添加在页面的 `` 中 ``` javascript /** * @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 ``` javascript 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(); ``` * 定义皮肤样色对象,然后根据对象生成样式,然后插入到页面中 * 多个皮肤,就多个皮肤颜色对象 #### Menu 其实这本来也不是什么有价值的东西。但是用 `React` 实现的,对于没做过类似的,算是个参考吧。 menu.js ``` javascript 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 小书` 的时候,也因为生命周期的问题,耽搁了一下。 * 首先提供一下[官网说明](http://react.css88.com/docs/react-component.html),中文的 * `componentWillMount()` 这个就不用了,挂载之前的逻辑,写到 `constructor()` 里 * `componentWillUpdate()` `componentWillReceiveProps()` 这两个不用了,写到 `static getDerivedStateFromProps()` ### 后期 发现前面该开发的,都写差不多。后期也没什么了。 * 我写了两份文档,`DOCS.md` 和 `STANDARD.md` `DOCS.md` 是关于相关插件,函数的使用和说明 `STANDARD.md` 是这个项目的代码规范文档,部分针对这个项目,多数都是普遍的规范 * 要求打包之后,还要灵活的配置 接口公共路径 public 中添加一个文件,`env.js` ,然后在 index.html 中手动引入 env.js ``` javascript window.PROJECT_CONFIG = { production: "http://..." } ``` 就是把变量放到 `window` 下,这样代码里就能 `window.PROJECT_CONFIG.production` 取到想要的数据,打包后,这个文件也一样存在,可以更改。环境变更的时候,就不用改代码,然后再打包了。 ### 为何没有使用 Redux ? 这是部分页面 ![部分界面](https://user-gold-cdn.xitu.io/2018/9/4/165a3f3c8b38dbf6?w=1540&h=815&f=png&s=68937) 图中两个组件,是有频繁交互的。当时是考虑用 `redux` 和 `react-redux` 来写。而我也实现了。 但是后来思前想后,觉得这种方式,如果让组里其他前端开发的话,不是那么友好。会写很多 `map...` 于是,我把右侧菜单,嵌套进了中间的组件中,外面看着没变化。但通信方式,已经变成了父子组件之间的通信方式。这就比较简单了。 这种处理方式,我认为是,用结构,来取代复杂的逻辑。 是的,每个页面,如何需要,都要带有自己的右侧菜单,仁者见仁智者见智吧。 ## 特别感谢 **这个一定要说在前面**,在学习 `React.js` 的开始,就有小伙伴推荐了 [react 小书](http://huziketang.mangojuice.top/books/react/), 作者[胡子大哈](https://www.zhihu.com/people/hu-zi-da-ha/activities)。这本书称得上是 **良心巨制** 。(截至目前,看了四遍)

    相关文章: