阅读本文你需要了解(pre-knowledge)

  1. dvajs框架
  2. 了解eslint
  3. 了解git hooks

目录

  1. 前言
  2. tslint和precommit
  3. 多语言支持(i18n)
  4. 动态加载js模块

前言

在上一篇文章中, 我们搭建了一个基本的dvajs框架, 并且加上了less和css模块化的应用, 最后还引入了antd组件库.
本文将以上一篇的项目为基础, 继续增加一些基础性的功能.

tslint和precommit

tslint

说到eslint, 经常会谈之色变, 谁没有感受过被eslint统治的恐惧呢. 不过恐惧归恐惧, eslint为我们代码的规范化还是作出了很多贡献的. 接下来为我们的项目配置tslint.

  • 首先, 当然要安装tslint. npm install --save-dev tslint.
  • 然后在项目的根目录下增加一个tslint.json的配置文件(也可以全局安装tslint, 然后执行tslint --init生成默认的配置文件). 关于tslint的配置信息, 请参照tslint configuration
    {
      "defaultSeverity": "error",
      "extends": [
          "tslint:recommended"
      ],
      "jsRules": {},
      "rules": {},
      "rulesDirectory": []
    }
    
  • 在package.json中加入一个lint的script: "lint": "tslint --fix -t msbuild -c tslint.json 'src/**/*.ts'", 然后执行npm run lint就可以看到tslint的报错信息了.

tslint 报的错误有时候让我不知所措, 这时候可以去google一下错误名称, 就可以知道官方的建议以及该建议的原因.
还有一些错误可能只是代码风格上的不同, 你可以调整错误级别或者修改这些规则的配置.

precommit

lint和git的precommit hook可以说是很完美的搭档了. 在多人协作的项目中, 能节省主程序猿大量review代码的时间.

  • 为了实现git的hook, 我们需要使用一个工具npm install --save-dev husky.
  • 增加一个precommit的script到package.json中: "precommit": "npm run lint".
  • 然后在package.json中增加husky的配置信息.
    "husky": {
      "hooks": {
        "pre-commit": "npm run precommit"
      }
    },
    
  • 这样, 在每次commit代码时, git就都会调用tslint来check代码有没有规范问题了.

使用git init将我们的项目初始化为一个git仓库.

多语言支持(i18n)

接下来为我们的项目增加多语言支持, 这里我们用到一个第三方的插件, npm install --save-dev react-intl-universal.
关于使用react-intl-universal的几点分析如下:

  • 根据插件的文档描述, 我们知道这个插件只需要全局初始化一次(加载语言文件), 然后就可以在项目中随意的使用了.
  • 我们需要在项目加载语言文件时, 提供给用户一个loading的状态. 因此我们在项目的入口处(router文件)处理react-intl-universal的初始化和项目的loading状态.
  • 为了不污染router组件, 我们将多语言处理的初始化封装到一个IntlProvider的组件中.

下面我们来实践一下:

  • 新建文件src/components/IntlProvider/IntlProvider.tsx.
    import * as React from 'react';
    import intl from 'react-intl-universal';
    import { Spin } from 'antd';
    
    import EnData from '../../locales/en-US';
    import CnData from '../../locales/zh-CN';
    
    // locale data
    const locales = {
        "en-US": EnData,
        "zh-CN": CnData,
    };
    
    interface IState {
        loadingLocales: boolean,
    }
    
    interface IProps {
    
    }
    
    export default class IntlProvider extends React.Component<IProps, IState> {
    
        constructor(props: IProps) {
            super(props);
            this.state = {
                loadingLocales: false,
            }
        }
    
        componentDidMount() {
            this.setState({
                loadingLocales: true,
            })
            this.loadLocales();
        }
    
        loadLocales() {
            intl.init({
                currentLocale: 'en-US',
                locales
            }).then(() => {
                this.setState({
                    loadingLocales: false,
                })
            })
        }
    
        render() {
            const { children } = this.props;
            const { loadingLocales } = this.state;
            if (loadingLocales) {
                return (<Spin spinning={true} />)
            }
            return (
                <div>
                    {children}
                </div>
            )
        }
    }
    
  • 创建语言资源文件src/locales/zh-CN.jssrc/locales/en-US.js, 文件内容很简单, 就是一个map.
    // src/locales/zh-CN.js
    export default {
      "hello": "你好"
    }
    
  • src/router.tsx中引用IntlProvider组件.
    // src/router.tsx
    import * as React from 'react';
    import { Router, Route, Switch, Redirect } from 'dva/router';
    import * as H from 'history';
    import Index from './routes/Index';
    // 引入IntlProvider组件
    import IntlProvider from './components/IntlProvider';
    
    function RouterConfig({ history }: { history: H.History }): JSX.Element {
        return (
            <IntlProvider>
                <Router history={history}>
                    <Switch>
                        <Route path="/" component={Index} />
                    </Switch>
                </Router>
            </IntlProvider>
        );
    }
    
    export default RouterConfig;
    

    为了简化IntlProvider组件的import路径, 我增加了一个src/components/IntlProvider/index.ts文件

    import IntlProvider from './IntlProvider';
    
    export default IntlProvider;
    

    在组件很多的情况下, 也可以用这种方式来集成导出组件, 这样引用组件时会变得清晰很多.

  • 接下来在我们的src/routes/Index.tsx组件中使用一下多语言功能.
    // src/Index.tsx
    import * as React from 'react';
    import * as ReactDOM from 'react-dom';
    import intl from 'react-intl-universal';
    
    export default class Index extends React.Component {
        render() {
            return <div>{intl.get('hello')}, react</div>
        }
    }
    
    

    到这里, 我们已经可以看到页面上展示了 hello, react.

  • 最后我们还需要做一件事情, 就是把切换语言的api暴露出来. 修改IntlProvider组件中的loadLocales方法:
    getCurrentLocale() {
        const currentLocale = intl.determineLocale({
            urlLocaleKey: 'lang',
            cookieLocaleKey: 'language',
        });
        return currentLocale;
    }
    
    loadLocales() {
        intl.init({
            currentLocale: this.getCurrentLocale(),
            locales
        }).then(() => {
            this.setState({
                loadingLocales: false,
            })
        })
    }
    

    查看intl.determineLocale的方法说明可以知道, react-intl-universal有一个默认获取当前语言的机制, 首先从url取urlLocaleKey这个参数, 然后从cookie取cookieLocaleKey这个参数. 这个机制基本够用, 当然你也可以改写getCurrentLocale方法来实现自己的获取当前语言的方法.

到此位置, 我们的多语言支持也实现完成了, 你可以通过http://localhost/?lang=zh-CNhttp://localhost/?lang=en-US分别看到中文和英文版本的{hello}, react.

动态加载js模块

近几年来, 浏览器的功能越来越强大, 前端应用也变得越来越”富有”, 我们的前端项目自然变得越来越庞大. 这时候, 动态加载资源就显得越来越重要了. 你可以想象如果打开一个web页面需要加载一个10MB的文件会劝退多少的用户.
dvajs为我们提供了动态加载模块的api, 接下来我们就要将这个功能应用到我们的项目中去.

  • 为了试验模块的动态加载, 我们必须至少有两个模块, 下面为我们的项目增加两个模块. 并且在router.tsx中定义路由信息.
    // src/routes/User/Login.tsx
    import * as React from 'react';
    
    export default class Login extends React.Component {
        render() {
            return <div>login module.</div>
        }
    }
    
    // src/routes/User/Register.tsx
    import * as React from 'react';
    
    export default class Register extends React.Component {
        render() {
            return <div>register module.</div>
        }
    }
    
    // src/router.tsx
    import Login from './routes/User/Login';
    import Register from './routes/User/Register';
    //...
    <Switch>
        <Route path="/login" component={Login} />
        <Route path="/register" component={Register} />
        <Route path="/" component={Index} />
    </Switch>
    //...
    

    这样, 就能访问我们的三个路由/, /login, /register了.

  • 下面修改router.jsx以支持动态加载.
    // src/router.tsx
    import * as React from 'react';
    import { Router, Route, Switch, Redirect } from 'dva/router';
    import dynamic from 'dva/dynamic';
    import { DvaInstance } from 'dva';
    
    import * as H from 'history';
    import Index from './routes/Index';
    import IntlProvider from './components/IntlProvider';
    
    function RouterConfig({ history, app }: { history: H.History, app: DvaInstance }): JSX.Element {
    
        const Login = dynamic({
            app,
            models: () => [
            ],
            component: () => import('./routes/User/Login')
        });
    
        const Register = dynamic({
            app,
            models: () => [
            ],
            component: () => import('./routes/User/Register')
        });
    
        return (
            <IntlProvider>
                <Router history={history}>
                    <Switch>
                        <Route path="/login" component={Login} /> 
                        <Route path="/register" component={Register} />
                        <Route path="/" component={Index} />
                    </Switch>
                </Router>
            </IntlProvider>
        );
    }
    
    export default RouterConfig;
    

    这时候编译报了一个无法解析import的错误, 我们需要为babel增加一个插件babel-plugin-syntax-dynamic-import, 使得babel支持解析import()的语法.
    执行npm i --save-dev babel-plugin-syntax-dynamic-import, 并且在.babelrc中加入这个plugin.

  • 这时候运行我们的项目, 可以看到login和register分别对应了一个异步的js文件, 也就是我们已经基本实现动态加载模块了.

    目前dvajs关于dynamic方法的类型声明文件(d.ts)有问题, 参考dva/#1758. 所以ts会报在dynamic方法上报类型错误, 这不影响我们的使用.
    我们可以手动覆盖该声明文件来解决这个报错, 参考Typescript/#11137. 在项目目录下新增一个文件src/types/dva/dynamic.d.ts, 内容如下:

    import { DvaInstance } from "dva";
    import { ComponentType } from "react";
    
    declare const dynamic: (opts: {
        app: DvaInstance,
        models?: () => Array<PromiseLike<any>>,
        component: () => PromiseLike<any>,
    }) => ComponentType<any>;
    export default dynamic;
    

    并且修改tsconfig.json, 为dva/dynamic单独指定声明文件的路径.

    {
        "compilerOptions": {
          "baseUrl": ".",
          "paths": {
            "dva/dynamic": [
                "src/types/dva/dynamic"
            ]
          }
        },
    }