从零搭建一个react门户网站

内容纲要

前言

开始使用react开发, 做了一个门户网站, 记录一下相关的内容

技术栈

  • react
  • react-router
  • typescript
  • less
  • babel
  • webpack

使用组件库

  • 多语言: react-intl
  • 浏览器兼容性: babel-polyfill
  • modal组件: react-modal
  • 图片放大: react-zmage

编码规范设计

结构

总体结构
src/
├── best/
├── com/
├── entries/
├── images/
├── scripts/
├── styles/
├── types/
├── dev.html
└── dist.html
  • best与com 与后台接口交互有关, 自动生成

  • entries: 页面入口

  • images: 图片资源

  • scripts: js内容

  • styles: 未使用css module, css 代码在该文件夹中

  • types: TS相关类型定义

js代码结构
scripts/
├── commons/
├── components/
├── consts/
├── locale/
├── pages/
├── remotes/
├── router/
└── utils
  • commons: 与业务有关的公共方法
  • components: 组件模块
  • consts: 常量
  • locale: 语言文件
  • pages: 页面
  • remotes: ajax方法
  • router: 路由
  • utils: 与业务无关的公共方法: 时间转换等

命名

  • 文件夹命名: 模块\页面等使用使用 Pascal 命名法,其他使用小写

  • 文件命名:使用 Pascal 命名法

  • 类命名: 使用 Pascal 命名法

  • 变量命名:使用 Camel 命名法。

  • 常量命名: 使用全部字母大写,单词间下划线分隔的命名方式

  • 语言字符串变量命名

    • 语言包命名规则: 模块_内容_类型
    • 模块分类包括:公共、menu、submenu 以及单个页面等
    • 内容即表示文字具体的含义
    • 类型包括:title、text等, 如果一项内容中存在多个单词,用Camel进行命名,如:menu_aboutUs_title

注释

  • 模块: 多行注释
/**
* 页面的tab组件,使用如下:
* let tabs: ITab[] = [{
    text: 'tab_boardOfDirectors_text',
    component: ()=> {
        return (
        <Directors></Directors>
        )
    }
    }]
<Tabs tabs={tabs}></Tabs>
*/
  • 模块定义
  • 使用范例
  • 注意事项( 可选 )
  • 函数(模块及公共方法): 多行注释
/**
* 渲染表格中的扩展行或者行
*/
  • 函数含义或者模块含义
  • 变量注释(因为用ts, 大部分类型不用注明, 变量含义不明确时备注)
  • 变量或逻辑解释: 单行注释, 简单明确, 复杂算法类可用多行详细说明

  • 备注: TS的使用可以让大部分函数参数和变量不用注释, 不提倡过分注释

函数

  • 注释: 见上
  • 命名:
    • 与语义有关, 区分获取数据\事件操作等
    • 静态函数加_表示
  • 行数不超过50行
  • 一个函数最好执行一个功能,不要多个功能代码混合在一起

其他

  • 使用了tslint配置, 但未统一团队内部使用插件, 导致部分格式化不一致(如tab和空格的使用)

模块设计

路由

  • 思路:

    • 使用react-router组件
    • 未使用服务端渲染, 在App组件内加入页面路由
    • 单独实现router组件, 将路由与对应的组件映射到router下
    • 在menu中填充路由地址, link到正确的页面
  • 实现

    • 在app下加入页面根路由
<IntlProvider locale={this.state.lang} messages={fields} defaultLocale={this.state.lang}>
<Router>
<HeadComp changeLanguage={this.changeLanguage} />
<MyRouter />
<FootComp />
</Router>
</IntlProvider>
  • 单独实现myRouter组件,赋值HashRouter

    • 定义router对象接口
    // router对象
    interface IRouterObj {
        path: string
        component: React.ReactNode
        isExact?: boolean
    }
    • router对象接口, 以path\component\isExact为属性, 对router组件进行赋值
    const layout = (
        <Switch>
            {routerData.map((item, i) => {
                return <Route key={i} path={item.path} 
                exact={item.isExact ? true : false} 
                component={item.component} />
            })}
        </Switch>
    )
    • 组装至router中

      {layout}
  • 实现menu组件, 在menu组件中处理路由跳转

    
           ${this.state.showMenuItem ? 'menu_hover' : ''} ${props.cls} }   activeClassName='menu_hover'>
          
           
    
  • 问题

    • 新闻类问题:

    • fb分享问题

      • fb分享出来的链接会去掉路由中的#, 本项目为单页面应用, 如此, 只能手动设定一个特别的路由去匹配,然后再跳转到正确的页面
      /**
       * 新闻页匹配字符串,自动添加#,避免因其他网站跳转删去#引发问题
       */
      export const newsDetailUrlType = '?newsDetail='
      /**
       * 处理facebook跳转时因为facebook网站删去#而引发的问题
       */
      export function setNewsDetailUrl(history: History) {
        let url = document.location.search
        if (url.indexOf(newsDetailUrlType) !== -1) {
      
          url = decodeURIComponent(url)
          let newsUrl = url.replace(newsDetailUrlType, '')
          //facebook 会增加该查询条件,影响单页面路由解析,去除该部分内容
          newsUrl = newsUrl.split('&fbclid')[0]
          document.location.search = ''
          //直接使用localtion.href替换的话,在谷歌下面会先渲染首页,再跳转到新闻页,看起来有两次渲染的效果,优化后去除
          history.push(newsUrl)
        }
      }
      • &fbclid 是fb分享之后加上的, 域名下才存在该问题, 只能再修改下
    • 关键字与描述类型只能动态更改

      • 之前对fb\tw之类实现分享, 但是必须要描述与关键字, 动态更改也无法用, 只能后续更改为服务端渲染再看
    • 路由字符串更改问题

      • menu的需求,当然涉及刷新页面, 点中的menu是需要hover颜色的, 本次是根据路由的路径来匹配对应的菜单
      render(){
        let { data: menuItems, location: { pathname } } = this.props
        setNewsDetailUrl(this.props.history)
        let baseUrl = pathname.split('/')[1]
        setPageTitle(baseUrl)
        return (
          
        ${this.props.cls} menu_ul}> { menuItems.map((item, i) => { //利用基准路由与当前url匹配,设置正确的activeUrl let selfBaseUrl = item.url.split('/')[1] return ( ) }) }
      ) }
    • script加载问题

    • 问题原因: 单页面应用切换路由时, 页面内容实际没有重新加载,

    • 处理方式: 第一次加载时页面内容保存加载出来的dom内容,后续直接使用该内容

    • 后续问题: 如果第一次js没有加载完, 在页面内部一直切换也不会再次加载, 除非刷新页面

    • link参数问题

双语

  • 思路

    • 使用统一字符串来作为页面文字的key值,对应的value作为各个语言的具体文字
    • 使用react-intl
    • 利用cookie, 设置当前页面语言类型及方法请求头参数, 获取对应的语言类型
    • 一个是不需要从远程获取的, 如menu文字等, 一个需要从远程获取对应内容的文字, 如history内容等
  • 实现

    • 在app 组件, 初始化语言包, 并在根组件使用intl的IntlProvider组件, 将语言包配置在整个应用程序中

    • 在app增加切换语言方法, 并用传prop形式传入切换组件

    /**
     * 切换语言类型
     */
    changeLanguage = (lang: keyof ILocales) => {
      let current = this.state.lang;
      if (current !== lang) {
        storage.setCookie('ir_lang', lang)
        this.setState({
          lang
        });
        //切换语言时重新刷新页面
        location.reload()
      }
    }
    render() {
      app.language.currentLang = this.state.lang
      const fields = app.language[this.state.lang]
      return (
        
          
            {/* 传入切换方法 */}
            
            
            
          
        
      );
    }
    • 用app.language 保存所有语言包内容及当前语言类型, 在不可以使用react-intl组件的情况下使用该值代替, 如下, 获取时间格式时, 无法使用react-intl的方法, 自己添加便需要获取一下
    /**
     * 返回形如:Jan 01, 2019 这样的数据,后续如果中文有特殊要求的话就再针对中英文进行修改
     * @param occureTime 
     */
    export function getDateString(occureTime: I__dateTimeTrans) {
        let lang = app.language.currentLang
        // 英文下:如Jan 01, 2019
        if (lang === 'en') {
            let date = new Date(occureTime).toDateString().split(' ')
            let year = date[date.length - 1]
            date.shift()
            date.pop()
            return date.join(' ') + ', ' + year
        } else {
            //中文:2019.01.01
            return getCalendarDate(occureTime, 'yyyy.MM.dd')
        }
    }
    • 页面使用

    • 直接显示文字时, 使用FormattedMessage组件即可, 还有其他的用法, 可以参考文档

    • 无法使用组件, 想直接获取到当前字符串对应的文字时, 如下, option的text 必须是纯文本, 无法用FormattedMessage来显示

      //因为页面部分文字不能直接用FormattedMessage组件,需要将语言包注入组件prop中
      export default injectIntl(EmailAlertsPage)
      /**
      * 渲染下拉框的option内容
      */
      renderOptions = (data: IoptionsItem[]) => {
      const { intl } = this.props
      let pleaseSel = intl.formatMessage({ id: 'email_pleaseSelect_text' })
      let res = [];
      res.push()
      res.push(
        data.map((item, i) => {
          let text = intl.formatMessage({ id: ${item.name}, defaultMessage: ${item.name} })
          return (
            // 订阅邮件下发key值错误
            
          )
        })
      )
      return res
      }
  • 问题

    • 更改语言时, 页面需重新加载(部分不能使用 react-intl )

    • 针对页面的语言包可以获取的内容, 切换语言时, react-intl 会帮助直接改变, 但是页面需要服务端数据时, 必须触发页面刷新, 才可以重新渲染页面整体

    • 目前使用的方法是强制刷新页面, 但感觉不是特别合适

      //切换语言时重新刷新页面
      location.reload()
    • 不需要组件, 需要单纯获取语言的内容

    • 如上面的获取当前时间显示的方法, 因为react-intl 获取页面数据, 需要将intl 作为参数注入的react 类中

      import { FormattedMessage, injectIntl, InjectedIntlProps } from 'react-intl'
      class EmailAlertsPage extends React.Component{}
    • 目前方法如上, 使用全局变量, 感觉也不是特别好

组件设计

tab切换

需求

20190908-react-tabs

20190908-react-tabs_active

  • 一个是固定的tab, 切换显示内容即可

  • 一个是数据填充的tab, 根据切换显示正确的tab页内容

实现
  • 简单版本实现

    render() {
    let { tabs } = this.props
    let currentComp = tabs[this.state.activeCompInd].component
    return (
      
      {tabs.map((item, i) => { return })}
    {currentComp()}
    ) }
    • tab数据与component一起从外部传入, 内容渲染tabs内容与被点击的当前tab内容即可
  • 数据切换, 其实数据填充与tab类似, 主要是tabs的实现有所改变

    /**
    * 获取tabs的第一个和最后一个tab的索引值,根据这两个进行渲染
    */
    getFirstLastInd = () => {
    let { tabs } = this.props
    let { activeCompInd } = this.state
    let showItemLen = this.showItemLen;
    let len = tabs.length - showItemLen
    let firstInd = 0;
    let lastInd = 0;
    if (len <= 0) {
      lastInd = tabs.length - 1
    } else {
      //不在最前面
      if (activeCompInd !== 0) {
        //是否在最后,不在最后-1 ,在最后减去showItemLen -1;
        firstInd = activeCompInd === tabs.length - 1 ? activeCompInd - (showItemLen - 1) : activeCompInd - 1
      }
      lastInd = firstInd + showItemLen - 1;
    }
    return {
      firstInd,
      lastInd
    }
    }
    
    /**
    * 渲染tabs内容
    */
    renderTabs = () => {
    let { tabs } = this.props
    let { activeCompInd } = this.state
    let { firstInd, lastInd } = this.getFirstLastInd();
    
    let getArrows = (index: number) => {
      return (
        
      )
    }
    let getArrowTab = (item: T, index: number) => {
      let { renderText } = this.props
      let text = renderText(item);
      return (
        
  • { this.changaActiveComp(index) }}> {index === firstInd ? getArrows(index) : ''} ${text}} defaultMessage={${text}} > {index === lastInd ? getArrows(index) : ''}
  • ) } let getTab = () => { var res = []; //当数据量小于2时,页面不展示tab页 if (tabs.length < 2) { return null; } for (let i = firstInd; i <= lastInd; i++) { res.push(getArrowTab(tabs[i], i)) } return res } return (
      {getTab()}
    ) }

遇到的奇奇怪怪问题

iframe加载

  • 目的: 在react中使用iframe加载

    render() {
    let { showIframe } = this.state
    return (
      
    {this.getLoading()}
    ${showIframe ? 'block' : 'none'} }} >
    ${showIframe ? 'block' : 'none'} }} >
    ) }
  • 实现: 固定宽高, 直接使用, onload 可以用来判断当前iframe是否加载上, 可以用来填充加载图标

script加载问题

  • 目的: 在react中,加载js内容, 并通过该js加载其他内容

  • 实现

    /**
     * 刷新页面时获取PressReleases的元素内容
     */
    getPressDiv = () => {
    let el = document.createElement('div')
    let wd_widget = document.createElement('div')
    wd_widget.className = 'wd_widget'
    wd_widget.dataset.wd_widgetId = 'JGD4fX6HgrCA'
    wd_widget.dataset.wd_widgetHost = '//bestinc.investorroom.com'
    let loadingDiv = document.createElement('div')
    loadingDiv.className = 'page_loading'
    wd_widget.appendChild(loadingDiv)
    el.appendChild(wd_widget)
    
    let scr = document.createElement('script')
    scr.src = '//bestinc.investorroom.com/js/wd_widgets.js'
    scr.async = true
    el.appendChild(scr)
    return el
    }
    componentDidMount() {
    //pressDiv不存在时,会将数据内容暂存pressDiv上
    if (!pressDiv) {
      pressDiv = this.getPressDiv()
    }
    this.selfElement.appendChild(pressDiv)
    }
    render() {
    return (
      
    (this.selfElement = el)}>
    ) }
    • 生成script标签
    • 利用常量, 如果当前div没有内容时, 生成script加载, 如果有, 直接显示div内容
  • 问题

    • html加载script标签, 只有第一次有效, 在单页面应用中, 第二次加载都无效
    • 目前改过的方法可以保证大部分问题, 但是如果第一次数据未加载完, 后面切换页面都不会有展示, 只能刷新页面重新加载

分享相关

  • url 无法展示

  • # 被自动删去

  • 生成的url 被增加后缀

    /**
    * 新闻页匹配字符串,自动添加#,避免因其他网站跳转删去#引发问题
    */
    export const newsDetailUrlType = '?newsDetail='
    /**
    * 处理facebook跳转时因为facebook网站删去#而引发的问题
    */
    export function setNewsDetailUrl(history: History) {
      let url = document.location.search
      if (url.indexOf(newsDetailUrlType) !== -1) {
    
          url = decodeURIComponent(url)
          let newsUrl = url.replace(newsDetailUrlType, '')
          //facebook 会增加该查询条件,影响单页面路由解析,去除该部分内容
          newsUrl = newsUrl.split('&fbclid')[0]
          document.location.search = ''
          //直接使用localtion.href替换的话,在谷歌下面会先渲染首页,再跳转到新闻页,看起来有两次渲染的效果,优化后去除
          history.push(newsUrl)
      }
    
    }

字体模糊问题

  • 3D动画
/**
 * 获取移动动画样式
 */
getTransitionStyle(ms: number, x: number) {
  return {
    transition: `transform ${this.props.edgeEasing} ${ms / 1000}s`,
    'WebkitTransition': `transform ${this.props.edgeEasing} ${ms / 1000}s`,
    transform: `translateX(-${x}px)`,
    'WebkitTransform': `translateX(-${x}px)`,
  }
}

平行四边形(实现与问题)

需求

20190908-react-menu1

  • hover区域为平行四边形
  • 文字显示正常
实现
//外部使用transform(定义沿着 X 轴的 2D 倾斜转换), 对内部span进行反方向旋转, 调整旋转角度
.nav_externaLink {
        width:180px;
        box-sizing: border-box;
        text-align: center;
        line-height: 40px;
        background-color: @menu_navFrightSelect_bg;
        transform: skewX(-@menu_navFrightAngle_size);
        float: left;
        border: none;
        cursor: pointer;
        span {
            display: inline-block;
            transform: skewX(@menu_navFrightAngle_size);
            color: @menu_navFrightSelect_color;
                }
}
问题

20190908-react-menu2

  • 因为是直接旋转的x轴, 看见的区域已经不在范围内, hover进入该区域, 会出现下方区域无法保持显示, 给用户造成困扰
解决方案
  • 给绿色图框区域添加填充物

  • ${this.state.showMenuItem ? 'menu_hover' : ''} ${props.cls} } activeClassName='menu_hover' > {/* 下拉倾斜角与原menu之间会有空隙,需要填充物 */}
    ${this.state.showMenuItem ? 'submenu_show' : 'submenu_hidden'} submenu_over} > {this.getCoverMenu(props)}
    ${this.state.showMenuItem ? 'submenu_show' : 'submenu_hidden'} submenu_block} > {submenu(props)}
  • .submenu_over {
    position: absolute;
    background: transparent;
    width: @menu_block_width;
    margin-left: -20px;
    div {
      height: 40px;
    }
    }

    20190908-react-menu3

  • 然后也要注意, div个数不可过多, 导致右下角点击也显示hover区域是有问题的, 所以增加了限制, 最多为五个

    /**
    * 获取用于填充的内容,大于5个div的部分不用填充
    */
    getCoverMenu = (props: IMenuLevelProps) => {
    if (props.subMenus) {
      return props.subMenus.map((item, i) => {
        if (i > 5) {
          return null
        } else {
          return (
            
    ) } }) } }

富文本加载阻塞问题

问题

在集成后的后台管理的新闻页, 加载富文本之前刷新页面列表, 切换到新增新闻页, 富文本一直加载不出来, 而本地富文本加载很快

原因

观察原因: 富文本需要的图片加载时间过长, 导致加载很慢, 浏览器对图片的并行加载机制一般是6个, 但刷新新闻页时, 图片列表要多很多,而且由于服务器在美国, 我们本地打开加载要慢很多 , 导致富文本图片加载被阻塞

处理

关闭了新闻列表的图片显示功能

视频加载问题

问题

谷歌浏览器升级到76之后, oss 路径的视频无法加载, 播放报错

20190908-react-video1

不正常的地址, 获取的数据

20190908-react-video2

正常的视频数据获取

20190908-react-video3

原因

oss传过来的content-type不对, 谷歌在升级之后对这种情况不进行兼容, 在开发中, 后续要注意content-type 可能导致的问题

发表评论

邮箱地址不会被公开。 必填项已用*标注