从0到1设计一个react-spa后台应用

下面围绕下面这张图,谈谈如何构建一个基本的react-spa应用框架。 css

image.png

按需加载

webpack3 + react-router4 + react-loadable

使用SPA必然要说到按需加载,目前最简洁优雅的方案是使用webpack3 + react-router4 + react-loadable, 原理就是 webpack 的 Dynamic Imports。html

通俗的讲,dynamic import,就是把JS代码分红N个页面份数的文件,不在用户刚进来就所有引入,而是等用户跳转路由的时候,再加载对应的JS文件。这样作的好处就是加速首屏显示速度,同时也减小了资源的浪费。react

webpack 的 Dynamic Imports 实现主要是利用 ECMAScript的 import() 动态加载特性,用于完成动态加载即运行时加载,而 import() 目前只是一个草案,若是须要用此方法,须要引入对应的转换器,如 babel-plugin-syntax-dynamic-import。webpack

react-loadable是一个高阶组件,参照官方文档Code Splitting,单页面的按需加载方案变得很是简洁:git

  1. 安装 babel-plugin-syntax-dynamic-import,为babel配置"syntax-dynamic-import"插件;
  2. 使用react-loadable
import Loadable from 'react-loadable';

import LoadingIndicator from 'components/LoadingIndicator';

const DataSandBox = Loadable({
  //自从webpack2.4开始,能够在动态导入中使用魔术注释来指定模块的chunk名字
  loader: () => import(/* webpackChunkName: "chunckName" */'../routers/module/index'),
  loading: LoadingIndicator
});

复制代码

LoadingIndicator是封装好的一个在异步加载阶段的loading展现,除此以外,react-loadable还提供了delay和timeout等配置项让按需加载的过程更加友好。github

Magic Comment

上文demo代码中说到的魔术注释值得说一下,这个是在webpack3新加上的。Webpack 2+ 开始,引入了Code Splitting-Async的新方法import(),用于动态引入ES Module。webpack将传入import方法的模块打包到一个单独的代码块(chunk),可是却不能像require.ensure同样,为生成的chunk指定chunkName,所以在webpack3中提出了Magic Comment用于解决该问题。web

publicPath

异步加载chunck文件须要利用publicPath来补全生产模式的cdn资源地址。参考城危同窗在这篇文章中的观点segmentfault

实践下来关于JSONP地址:"本地开发、平常开发、预发、线上”等环节有一个共同的特色,不管环境 怎么改变,chunk文件与主文件的相对路径是不会改变的,那获取runtime的JS地址便可肯定JSONP地 址,脱离环境、version和项目仓库名。浏览器

经过在页面入口文件中增长以下代码,能够兼容开发环境和生产环境对chunk文件的引用sass

/**
 * 设置 __webpack_public_path__, 兼容平常、预发、线上环境
 */
const js = document.scripts;
const url = js[js.length - 1].src.split("?")[0];
const urlSplit = url.split("/");
urlSplit.pop();
urlSplit.pop();
__webpack_public_path__ = urlSplit.join("/") + "/"; 
复制代码

Antd和React的版本

Antd 3.0实际上是在看了SEECONF上它山前辈的分享而被种草的。咱们但愿使用Antd 3.0的视觉风格,让后台总体看起来更加明亮,所以,将组件库升级为Antd 3.0,同时使用react v16。这里须要注意的是,由于antd2.x 默认是用12px, 而3.0 使用的是14px,若是升级的话,对2.x系列业务组件尺寸挑战,对于旧组件可能会有一些兼容成本,好比须要组件内部对默认字体作一下设定。

样式方案

CSS modules

CSS Evolution: From CSS, SASS, BEM, CSS Modules to Styled Components 比较全面的介绍了css技术的进化过程。

image.png
咱们须要寻求一个搭配当前的技术选型(React)的最优方案,解决两个问题:避免样式覆盖和便于实现样式的复用。

css不是程序语言,但若是说要给它加一个做用域的概念的话,那就是:只有全局做用域。

不管分拆为多少个css文件,不管用怎样的方式引入,全部的样式规则都位于同一做用域,只要选择符近似,就有发生覆盖的可能。

CSS Modules是一种技术流的组织css代码的策略,经过工具解决了BEM依靠开发人员选择惟一class名的工做,没法改变css全局做用域的本性,而是依靠动态生成class名这一手段(利用webpack的css-loader),来实现局部做用域。显然,这样的class名就能够是惟一的,无论本来的css代码写得有多随便,均可以这样转换获得不冲突的css代码。

要使用CSS Modules,必须想办法把变量风格的class名注入到html中,这时,虚拟DOM风格的React,搭配CSS Modules会很容易:有了CSS “本地做用域”,全部的 React 组件均可以在逻辑和呈现状态上进行彻底的隔离。

使用CSS Modules只须要在webpack中给css-loader加上以下两个参数:

名称 类型 默认值 描述
modules {Boolean} false 启用/禁用 CSS modules
localIdentName {String} [hash:base64] 配置生成的标识符(ident),推荐设置[local]___[hash:base64:5]

js 文件的改变就是在设置 className 时,用一个对象属性取代了原来的字符串。

import classNames from 'classnames';
import styles from './dialog.css';

export default class Dialog extends React.Component {
  render() {
    const cx = classNames({
      [styles.confirm]: !this.state.disabled,
      [styles.disabledConfirm]: this.state.disabled
    });

    return <div className={styles.root}>
      <a className={cx}>Confirm</a>
      ...
    </div>
  }
}
复制代码

如何与全局样式共存

在实际工程中,须要诸如reset/normalize,Settings等一些通用的全局设置。开启css modules设置后,全部的样式默认都是local模式,这时,可使用:global 标签在主应用程序中导入公共的样式文件。

image.png

覆盖组件样式

  • CSS Modules 不会覆盖属性选择器,因此能够利用属性选择器来解决这个问题;
  • 引入的 antd 组件类名没有被 CSS Modules 转化,因此被覆盖的类名,如 .ant-select-selection 必须放到 :global 中,为了防止对其余同类组件形成影响,能够给组件添加 className,只对这类组件进行覆盖,也能够利用外层类名实现这种限制。

路由与布局

数据驱动的路由配置

咱们须要两个对应关系,菜单和路由的关系以及路由和组件的关系,即经过url找到menu再加载组件这样一个过程。

url到组件的转换包括两个入口,一个是经过menu点击,一个是经过Link跳转。

Route能够帮咱们解决url到component的转换,即根据path来加载对应的component。那剩下的工做就是定义一个对象来存储关系,并实现一个经过url找到对应菜单项的方法。

参考Antd pro刚对内发布时候的源代码,能够设计一个公共的nav.js用来管理url、菜单和路由(模块组件)三者的关系。结合前文提到的按需加载策略,基本结构以下:

import BasicLayout from "components/Layouts/BasicLayout.js";
// 按路由拆分代码
import Loadable from "react-loadable";
import LoadingIndicator from "components/LoadingIndicator/LoadingIndicator";

//概览页
const DashBoard = Loadable({
  loader: () => import(/* webpackChunkName: "DashBoard" */"../routers/DashBoard/index"),
  loading: LoadingIndicator
});

/*将须要的路由组件包装成动态加载的形式,而后配置到navData数据结构里面*/
......

const navData = [
  {
    component: BasicLayout,
    layout: "BasicLayout",
    name: "首页", // for breadcrumb
    path: "",
    children: [
      {
        name: "概览",
        icon: "dashboard",
        path: "dashboard",
        component: DashBoard
      },
      {
        name: "特征管理",
        icon: "bars",
        path: "feature",
        children: [
          {
            name: "明星人脸库",
            icon: "star",
            path: "face",
            component: StarFaceManage,
          }
        ]
      },
      {
        name: "数据沙盘",
        icon: "play-circle",
        path: "sandbox",
        component: DataSandBox,
        isLink:true
      }
    ]
  }
];

export function getNavData() {
  return navData;
}

export { navData };
复制代码

name,icon是菜单的展现属性,path表明其对应的url片断,children提供菜单无限向下扩展的能力,只有叶子节点才有对应的component。经过这种结构,能够递归地渲染出对应的菜单结果。针对Link形式的跳转,将isLink设置为true,不在菜单的结构中显示,但能够经过path让route识别到。这样,造成了一个经过数据驱动的路由配置。

url到菜单的映射

url到菜单的映射就是:不一样的url对应的openKeys和selectedKeys属性是啥。 下面是一个基本(只提供一种布局)的主页面的代码结构:

import BasicLayout from "components/Layouts/BasicLayout";
import { Router, Switch, Route } from "react-router-dom";
import { createBrowserHistory } from "history";
const history = createBrowserHistory();

/**
 * 设置 __webpack_public_path__, 兼容平常、预发、线上环境
 */
const js = document.scripts;
const url = js[js.length - 1].src.split("?")[0];
const urlSplit = url.split("/");
urlSplit.pop();
urlSplit.pop();
__webpack_public_path__ = urlSplit.join("/") + "/";

/**
 * 基础信息配置 window.GV经过diamond配置
 */
const Globol_Values = window.GV || {};
//登录用户
const user = (Globol_Values.user && JSON.parse(Globol_Values.user)) || {};
const baseConfigs = {
  //平台logo
  siteLogo: Globol_Values.siteLogo || "",
  .......
};

const App = () => (
  <Router history={history}>
    <Switch>
      <Route
        path="/"
        render={props =>
           (
            <BasicLayout {...props} currentUser={user} {...baseConfigs} />
           )
        }
      />
    </Switch>
  </Router>
);

ReactDOM.render(<App />, document.getElementById("app"));
复制代码

Router会建立一个history对象并用其保持追踪当前location,在location有变化时对网页进行从新渲染。经过渲染的元素会被传入一些参数。分别是match对象,当前location对象以及history对象(由router建立)。locations 是一个含有描述URL不一样部分属性的对象,结构以下:

// 一个基本的location对象
{ pathname: '/', search: '', hash: '', key: 'abc123' state: {} }
复制代码

利用这个特性,BasicLayout在每次url变化时,能够接收父组件传入的props中的location对象,并经过pathname属性来进行menu的匹配。

基于 React Router 4 的可复用 Layout 组件

结合前文的设计,咱们但愿可以设计一个可复用 Layout 组件。

动态标题设置

React-document-title提供了一种声明式的方法来设置单页应用的的文档标题

基本布局

antd的Layout提供了基本的布局能力。仿照pro,咱们选择"侧边两列式布局。页面横向空间有限时,侧边导航可收起"的形式,同时自定义收起触发器。

undefined

const layout = (
   <Layout>
	 <Sider></Sider>
	 <Layout>
	    <Header></Header>
		<Content></Content>
	 </Layout>
    </Layout>
)
复制代码

Sider

Sider是侧边栏,功能就是展现菜单,同时能够根据横向空间展开收起。自定义触发器首先须要把trigger属性设置为null。breakpoint这个属性颇有意思,是触发响应式布局的断点,

//antd中对breakpoint 的规范定义 也是响应式栅格的边界
{
  xs: '480px',
  sm: '576px',
  md: '768px',
  lg: '992px',
  xl: '1200px',
  xxl: '1600px',
}
复制代码
<Sider
   trigger={null}
   collapsible
   collapsed={this.state.collapsed}
   breakpoint="md"
   onCollapse={this.onCollapse}
   width={256}
   className={styles.sider}
>
</Sider>
复制代码

breakpoint="md"即body的宽度大于768时,sider就会收起。样式上,sider的min-height须要设置为100vh,即默认高度占满整个浏览器的视窗。

参考pro的源码,咱们能够获得启发,sider能够经过breakpointer来动态的改变布局,那么根据antd的栅格规范,使用 react-container-query 动态给 layout 根据不一样的宽度加 classname,那么里面包含的全部dom均可以根据这个来调整样式。

import DocumentTitle from "react-document-title";
import { ContainerQuery } from "react-container-query";
//定义ContainerQuery的参数
const query = {
  "screen-xs": {
    maxWidth: 575
  },
  "screen-sm": {
    minWidth: 576,
    maxWidth: 767
  },
  "screen-md": {
    minWidth: 768,
    maxWidth: 991
  },
  "screen-lg": {
    minWidth: 992,
    maxWidth: 1199
  },
  "screen-xl": {
    minWidth: 1200
  }
};
复制代码

一个有动态标题和自适应能力的基本布局结构

<DocumentTitle title={this.getPageTitle()}>
   <ContainerQuery query={query}>
       {params => <div className={classNames(params)}>{layout}</div>}
    </ContainerQuery>
</DocumentTitle>

复制代码

Content

Content内展现路由组件的内容,咱们使用<Switch>组件来包裹一组<Route><Switch>会遍历自身的子元素(即路由)并对第一个匹配当前路径的元素进行渲染。将nav.js中定义的关系数据传入,生成这组Route结构。

<Content style={{ margin: "24px 24px 0", height: "100%" }}>
            <Switch>
              {getRouteData("BasicLayout").map(item => (
                <Route
                  exact={item.exact}
                  key={item.path}
                  path={item.path}
                  component={item.component}
                />
              ))}
              <Route
                path={"/forbidden/:routerName"}
                component={ForbiddenPage}
              />
              <Redirect exact from="/" to={defaultRoute} />
              <Route component={PageNotFound} />
            </Switch>
        </Content>
复制代码

总结

本文总结了一个react-SPA后台基本框架的设计过程,省略了不少设计细节,也不涉及状态管理方面的框架选型,只是对本身思考过程的一个回顾,但愿对感兴趣的同窗有帮助。

相关文章
相关标签/搜索