搭建dva的typescript开发环境(二)
阅读本文你需要了解(pre-knowledge)
- dvajs框架
- 了解eslint
- 了解git hooks
目录
前言
在上一篇文章中, 我们搭建了一个基本的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.js
和src/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-CN
和http://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" ] } }, }