github地址:github.com/bbwlfx/ts-b…
序
最近参与了很多迁库的工作,感觉很多老代码已经不太实用,并且存在一些漏洞,加上这点时间听了很多同事的分享,因此决定尝试一下构建React的最佳实践。
为了更好地了解react项目结构,锻炼自己的能力,这里并没有使用create-react-app
。
目标
- SPA + SSR
- 首屏数据加载
- 优雅的前后端同构
- 使用最新的技术栈
- ...
技术选型
-
React + @rematch
@rematch前段时间新出的redux框架,拥有比redux更简洁的语法,无需复杂的action creators和thunk middleware。
github地址:github.com/rematch/rem…
中文文档地址:rematch.gitbook.io/handbook/ap…
并且rematch本身支持immer插件,可以通过mutable的写法去管理状态的变化。
Model定义
model的定义就是redux的actions、reducer以及state,@rematch将三者合为一个model文件,每一个modal有三个属性:state、reducers、effects。
- state存放当前model的所有状态
- reducers存放各种同步修改state的函数,和redux的定义一样
- effects存放各种异步函数,并且不需要任何middleware,@rematch本身就支持async/await写法。
并且在effects中,我们可以通过dispatch去调用其他model的方法,去修改其他模块的数据。
// effects的两种写法 dispatch({ type: 'count/incrementAsync', payload: 1 }) // state = { count: 3 } after delay dispatch.count.incrementAsync(1) 复制代码
Modal.js
export const count = { state: 0, // initial state reducers: { // handle state changes with pure functions increment(state, payload) { return state + payload } }, effects: (dispatch) => ({ // handle state changes with impure functions. // use async/await for async actions async incrementAsync(payload, rootState) { await new Promise(resolve => setTimeout(resolve, 1000)) dispatch.count.increment(payload) } }) } 复制代码
Immer插件
const todo = { state: [{ todo: "Learn typescript", done: true }, { todo: "Try immer", done: false }], reducers: { done(state) { state.push({todo: "Tweet about it"}) state[1].done = true return state } } }; 复制代码
-
TypeScript
选择ts的最根本的愿意其实是因为js已经用烂了,打算尝试一下ts的使用,因为在网上也看到了很多关于ts优势的介绍。本着追求极致的原则选择使用了ts。
-
Koa@2
Koa本身是一个十分轻量的node框架,并且拥有丰富的第三方插件库以及生态环境,并且Koa本身的易扩展性让我们可以灵活开发,koa2支持的async/await语法也让异步请求写起来十分舒服。
-
Webpack@4
-
react-router@4
本身在前端路由方面选择了@reach/router,但是使用了一段时间之后发现经常会出现页面刷新之后突然滚动到另外的位置上,后来查资料发现@reach/router源码中使用了大量的光标操作,据说是为了对残疾人友好。这些光标操作不知道什么时候就会产生一些奇怪的bug,因此最终还是放弃了@reach/router选择了react-router。
@reach/router和react-router的区别在于:@reach/router是分型路由,支持我们以碎片化的方式定义局部路由,不必像react-router一样需要有一个大的路由配置文件,所有的路由都写在一起。这种分型路由在大型应用里面开发起来比较方便,但是同样也会产生不易维护的副作用。
-
pug
模板引擎选择了pug(jade),pug模板本身使用的是js语法,对前端开发人员十分友好,并且pug本身也支持非常多的功能。koa-pug中间件也支持pug引擎。
doctype html html head meta(http-equiv="X-UA-Compatible" content="IE=edge,chrome=1") meta(charset="utf-8") include ./common_state.pug block links | !{ styles } block common_title title TodoList include counter.pug block custom_state body block doms #root !{ html } | !{ scripts } 复制代码
-
react-loadable
目录结构
目录整体分为前端目录:public 、 后端目录:src
public的js目录中存放文件如下:
- components 组件代码
- constants 常量代码
- containers 页面代码
- decorators 各种装饰器的代码
- entry 页面入口代码
- lib 工具库代码
- models @rematch的Modal代码
- scripts 辅助脚本代码
- typings ts类型声明代码
src的目录如下:
- config 配置文件
- controllers 路由处理文件
- routes 路由声明文件
- template 模板文件
- utils 工具代码
- app.js 后端启动入口,主要存放运维代码
- server.js server启动代码,主要存放业务代码
webpack配置文件
webpack配置这里配合webpack-merge,做到webpack配置文件的拆分。
- webpack.base.config.js
- webpack.client.config.js
- webpack.dev.config.js
- webpack.prod.config.js
- webpack.ssr.config.js
base负责基本的配置
client负责前端打包的配置
ssr负责服务端渲染的打包的配置
dev负责开发模式的配置
prod负责生产模式的配置
具体的配置可以到项目源码中查看
其他配置文件
public和src目录都需要一个单独的.babelrc文件,由于babel7支持通过js的写法书写配置文件了,所以这里直接用两个.babelrc.js
文件即可。
public/.babelrc.js
module.exports = api => {
const env = api.env();
// 服务端渲染时不加载css
const importConfig =
env === "client"
? {
libraryName: "antd",
libraryDirectory: "es",
style: true
}
: {
libraryName: "antd"
};
return {
presets: [
[
"@babel/env",
{
modules: env === "ssr" ? false : "commonjs",
targets: {
browsers: ["last 2 versions"]
}
}
],
"@babel/react",
"@babel/typescript"
],
plugins: [
["import", importConfig],
"dynamic-import-node",
"@babel/plugin-proposal-class-properties",
[
"babel-plugin-module-resolver",
{
cwd: "babelrc",
extensions: [".ts", ".tsx"],
root: ["./"],
alias: {
components: "./js/components",
containers: "./js/containers",
models: "./js/models",
decorators: "./js/decorators",
constants: "./js/constants",
lib: "./js/lib",
typings: "./js/typings"
}
}
],
"react-loadable/babel"
]
};
};
复制代码
babel-plugin-import插件负责处理对antd的按需加载问题,并且处理ssr不加载css的逻辑。
dynamic-import-node插件负责处理服务端渲染时候对前端组件动态加载的处理。
module-resolver插件负责处理alias问题,由于webpack的alias只能在前端使用,服务端渲染的时候无法处理webpack中定义的alias,因此这里使用插件来解决这个问题。
src/.babelrc.js
module.exports = {
presets: [
[
"@babel/env",
{
targets: {
node: "current"
}
}
],
"@babel/react",
"@babel/typescript"
],
plugins: [
"@babel/plugin-proposal-class-properties",
"dynamic-import-node",
[
"babel-plugin-module-resolver",
{
cwd: "babelrc",
alias: {
components: "../public/js/components",
containers: "../public/js/containers",
models: "../public/js/models",
controllers: "./controllers",
decorators: "../public/js/decorators",
server: "./public/buildServer",
lib: "../public/js/lib",
typings: "./js/typings"
},
extensions: [".ts", ".tsx", ".js", ".jsx"]
}
]
]
};
复制代码
为了配合SSR,node层的.babelrc
文件也需要同样一套alias。
tsconfig.json
{
"compilerOptions": {
"sourceMap": true,
"outDir": "./dist/",
"moduleResolution": "node",
"jsx": "preserve",
"module": "esNext",
"target": "es2015",
"allowSyntheticDefaultImports": true,
"allowJs": true,
"lib": ["es2017", "dom"],
"baseUrl": ".",
"noEmit": true,
"pretty": true,
"skipLibCheck": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"resolveJsonModule": true,
"noImplicitReturns": true
},
"include": ["**/*.ts", "**/*.tsx"],
"exclude": ["node_modules", "**/*.spec.ts", "**/*.d.ts"]
}
复制代码
其他的还有一切开发的配置文件,比如.eslintrc,.stylelintrc等看个人喜好配置即可。
为了更好的格式化代码,以及在commit之前做一些校验工作,项目里添加了husky、lint-staged、prettier-eslint等npm包。 并且在package.json
文件中定义好对应的代码:
package.json
"scripts": {
"precommit": "lint-staged",
"format": "prettier-eslint --write public/**/*.{js,ts}"
},
"lint-staged": {
"*.{ts,tsx}": [
"npm run format --",
"git add"
],
"*.{js,jsx}": [
"npm run format --",
"git add"
],
"*.{css,less,scss}": [
"npm run format --",
"stylelint --syntax=less",
"git add"
]
}
复制代码
基本的配置到这里就结束了,下一章开始正式开发的介绍。
系列文章: