摘要:前段时间有写过一个在项目中的实践。在里边有解释了为什么要使用,以及在中的一个项目结构是怎样的。关于的配置文件,在本项目中存在两份。一个需要注意的小细节因为我们的与实现版本中都用到了。
前段时间有写过一个TypeScript在node项目中的实践。
在里边有解释了为什么要使用TS,以及在Node中的一个项目结构是怎样的。
但是那仅仅是一个纯接口项目,碰巧赶上近期的另一个项目重构也由我来主持,经过上次的实践以后,尝到了TS所带来的甜头,毫不犹豫的选择用TS+React来重构这个项目。
这次的重构不仅包括Node的重构(之前是Express的项目),同时还包括前端的重构(之前是由jQuery驱动的多页应用)。
因为目前项目是没有做前后分离的打算的(一个内部工具平台类的项目),所以大致结构就是基于上次Node项目的结构,在其之上添加了一些FrontEnd的目录结构:
. ├── README.md ├── copy-static-assets.ts ├── nodemon.json ├── package.json + ├── client-dist + │ ├── bundle.js + │ ├── bundle.js.map + │ ├── logo.png + │ └── vendors.dll.js ├── dist ├── src │ ├── config │ ├── controllers │ ├── entity │ ├── models │ ├── middleware │ ├── public │ ├── app.ts │ ├── server.ts │ ├── types + │ ├── common │ └── utils + ├── client-src + │ ├── components + │ │ └── Header.tsx + │ ├── conf + │ │ └── host.ts + │ ├── dist + │ ├── utils + │ ├── index.ejs + │ ├── index.tsx + │ ├── webpack + │ ├── package.json + │ └── tsconfig.json + ├── views + │ └── index.ejs ├── tsconfig.json └── tslint.json
其中标绿(也可能是一个+号显示)的文件为本次新增的。
其中client-dist与views都是通过webpack生成的,实际的源码文件都在client-src下。_就这个结构拆分前后分离其实没有什么成本_
在下边分了大概这样的一些文件夹:
dir/file | desc |
---|---|
index.ejs | 项目的入口html文件,采用ejs作为渲染引擎 |
index.tsx | 项目的入口js文件,后缀使用tsx,原因有二: 1. 我们会使用ts进行React程序的开发 2. .tsx文件在vs code上的icon比较好看 :p |
tsconfig.json | 是用于tsc编译执行的一些配置文件 |
components | 组件存放的目录 |
config | 各种配置项存放的位置,类似请求接口的host或者各种状态的map映射之类的(可以理解为枚举对象们都在这里) |
utils | 一些公共函数存放的位置,各种可复用的代码都应该放在这里 |
dist | 各种静态资源的存放位置,图片之类文件 |
webpack | 里边存放了各种环境的webpack脚本命令以及dll的生成 |
实际上边还漏掉了一个新增的文件夹,我们在src目录下新增了一个common目录,这个目录是存放一些公共的函数和公共的config,不同于utils或者config的是,这里的代码是前后端共享的,所以这里边的函数一定要是完全的不包含任何环境依赖,不包含任何业务逻辑的。
类似的数字千分位,日期格式化,抑或是服务监听的端口号,这些不包含任何逻辑,也对环境没有强依赖的代码,我们都可以放在这里。
这也是没有做前后分离带来的一个小甜头吧,前后可以共享一部分代码。
要实现这样的配置,基于上述项目需要修改如下几处:
1 src下的utils和config部分代码迁移到common文件夹下,主要是用于区分是否可前后通用
2 为了将对之前node结构方面的影响降至最低,我们需要在common文件夹下新增一个index.ts索引文件,并在utils/index.ts下引用它,这样对于node方面使用来讲,并不需要关心这个文件是来自utils还是common
// src/common/utils/comma.ts export default (num: number): string => String(num).replace(/B(?=(d{3})+$)/g, ",") // src/common/utils/index.ts export { default as comma } from "./comma" // src/utils.index.ts export * from "../common/utils" // src/app.ts import { comma } from "./utils" // 并不需要关心是来自common还是来自utils console.log(comma(1234567)) // 1,234,567
3 然后是配置webpack的alias属性,用于webpack能够正确的找到其路径
// client-src/webpack/base.js module.exports = { resolve: { alias: { "@Common": path.resolve(__dirname, "../../src/common"), } } }
4 同时我们还需要配置tsconfig.json用于vs code可以找到对应的目录,不然会在编辑器中提示can"t find module XXX
// client-src/tsconfig.json { "compilerOptions": { "paths": { // 用于引入某个`module` "@Common/*": [ "../src/common/*" ] } } }
5 最后在client-src/utils/index.ts写上类似server端的处理就可以了
// client-src/utils/index.ts export * from "@Common/utils" // client-src/index.tsx import { comma } from "./utils" console.log(comma(1234567)) // 1,234,567环境的搭建
如果使用vs code进行开发,而且使用了ESLint的话,需要修改TS语法支持的后缀,添加typescriptreact的一些处理,这样才会自动修复一些ESLint的规则:
"eslint.validate": [ "javascript", "javascriptreact", { "language": "typescript", "autoFix": true }, { "language": "typescriptreact", "autoFix": true } ]webpack的配置
因为在前端使用了React,按照目前的主流,webpack肯定是必不可少的。
并没有选择成熟的cra(create-react-app)来进行环境搭建,原因有下:
webpack更新到4以后并没有尝试过,想自己耍一耍
结合着TS以及公司内部的东西,会有一些自定义配置情况的出现,担心二次开发太繁琐
但是其实也没有太多的配置,本次重构选用的UI框架为Google Material的实现:material-ui
而他们采用的是jss 来进行样式的编写,所以也不会涉及到之前惯用的scss的那一套loader了。
webpack分了大概如下几个文件:
file | desc |
---|---|
common.js | 公共的webpack配置,类似env之类的选项 |
dll.js | 用于将一些不会修改的第三方库进行提前打包,加快开发时编译效率 |
base.js | 可以理解为是webpack的基础配置文件,通用的loader以及plugins在这里 |
pro.js | 生产环境的特殊配置(代码压缩、资源上传) |
dev.js | 开发环境的特殊配置(source-map) |
dll是一个很早之前的套路了,大概需要修改这么几处:
创建一个多带带的webpack文件,用于生成dll文件
在普通的webpack文件中进行引用生成的dll文件
// dll.js { entry: { // 需要提前打包的库 vendors: [ "react", "react-dom", "react-router-dom", "babel-polyfill", ], }, output: { filename: "vendors.dll.js", path: path.resolve(__dirname, "../../client-dist"), // 输出时不要少了这个option library: "vendors_lib", }, plugins: [ new webpack.DllPlugin({ context: __dirname, // 向外抛出的`vendors.dll.js`代码的具体映射,引用`dll`文件的时候通过它来做映射关系的 path: path.join(__dirname, "../dist/vendors-manifest.json"), name: "vendors_lib", }) ] } // base.js { plugins: [ new webpack.DllReferencePlugin({ context: __dirname, manifest: require("../dist/vendors-manifest.json"), }), ] }
这样在watch文件时,打包就会跳过verdors中存在的那些包了。
有一点要注意的,如果最终需要上传这些静态资源,记得连带着verdors.dll.js一并上传
在本地开发时,vendors文件并不会自动注入到html模版中去,所以我们有用到了另一个插件,add-asset-html-webpack-plugin。
同时在使用中可能还会遇到webpack无限次数的重新打包,这个需要配置ignore来解决-.-:
// dev.js const HtmlWebpackPlugin = require("html-webpack-plugin") const AddAssetHtmlPlugin = require("add-asset-html-webpack-plugin") { plugins: [ // 将`ejs`模版文件放到目标文件夹,并注入入口`js`文件 new HtmlWebpackPlugin({ template: path.resolve(__dirname, "../index.ejs"), filename: path.resolve(__dirname, "../../views/index.ejs"), }), // 将`vendors`文件注入到`ejs`模版中 new AddAssetHtmlPlugin({ filepath: path.resolve(__dirname, "../../client-dist/vendors.dll.js"), includeSourcemap: false, }), // 忽略`ejs`和`js`的文件变化,避免`webpack`无限重新打包的问题 new webpack.WatchIgnorePlugin([ /.ejs$/, /.js$/, ]), ] }TypeScript相关的配置
TS的配置分了两块,一个是webpack的配置,另一个是tsconfig的配置。
首先是webpack,针对ts、tsx文件我们使用了两个loader:
{ rules: [ { test: /.tsx?$/, use: ["babel-loader", "ts-loader"], exclude: /node_modules/, } ], resolve: { // 一定不要忘记配置ts tsx后缀 extensions: [".tsx", ".ts", ".js"], } }
ts-loader用于将TS的一些特性转换为JS兼容的语法,然后执行babel进行处理react/jsx相关的代码,最终生成可执行的JS代码。
然后是tsconfig的配置,ts-loader的执行是依托于这里的配置的,大致的配置如下:
{ "compilerOptions": { "module": "esnext", "target": "es6", "allowSyntheticDefaultImports": true, // import的相对起始路径 "baseUrl": ".", "sourceMap": true, // 构建输出目录,但因为使用了`webpack`,所以这个配置并没有什么卵用 "outDir": "../client-dist", // 开启`JSX`模式, // `preserve`的配置让`tsc`不会去处理它,而是使用后续的`babel-loader`进行处理 "jsx": "preserve", "strict": true, "moduleResolution": "node", // 开启装饰器的使用 "experimentalDecorators": true, "emitDecoratorMetadata": true, // `vs code`所需要的,在开发时找到对应的路径,真实的引用是在`webpack`中配置的`alias` "paths": { "@Common": [ "../src/common" ], "@Common/*": [ "../src/common/*" ] } }, "exclude": [ "node_modules" ] }ESLint的配置
最近这段时间,我们团队基于airbnb的ESLint规则进行了一些自定义,创建了自家的eslint-config-blued
同时还存在了react和typescript的两个衍生版本。
关于ESLint的配置文件.eslintrc,在本项目中存在两份。一个是根目录的blued-typescript,另一个是client-src下的blued-react + blued-typescript。
因为根目录的更多用于node项目,所以没必要把react什么的依赖也装进来。
# .eslintrc extends: blued-typescript # client-src/.eslintrc extends: - blued-react - blued-typescript
一个需要注意的小细节
因为我们的react与typescript实现版本中都用到了parser。
react使用的是babel-eslint,typescript使用的是typescript-eslint-parser。
但是parser只能有一个,从option的命名中就可以看出extends、plugins、rules,到了parser就没有复数了。
所以这两个插件在extends中的顺序就变得很关键,babel现在并不能理解TS的语法,但好像babel开发者有支持TS的意愿。
但就目前来说,一定要保证react在前,typescript在后,这样parser才会使用typescript-eslint-parser来进行覆盖。
除了上边提到的两端公用代码以外,还需要添加一个controller用于吐页面,因为使用的是routing-controllers这个库,渲染一个静态页面被封装的非常棒,仅仅需要修改两个页面,一个用于设置render模版的根目录,另一个用来设置要吐出来的模版名称:
// controller/index.ts import { Get, Controller, Render, } from "routing-controllers" @Controller("/") export default class { @Get("/") @Render("index") // 指定一个模版的名字 async router() { // 渲染页面时的一些变量 // 类似之前的 ctx.state = XXX return { title: "First TypeScript React App", } } } // app.ts import koaViews from "koa-views" // 添加模版所在的目录 // 以及使用的渲染引擎、文件后缀 app.use(koaViews(path.join(__dirname, "../views"), { options: { ext: "ejs", }, extension: "ejs", }))
如果是多个页面,那就创建多个用来Render的ts文件就好了
深坑,注意目前的routing-controller对于Koa的支持还不是很好,(原作者对Koa并不是很了解,导致Render对应的接口被请求一次以后,后续所有的其他的接口都会直接返回该模版文件,原因是在负责模版渲染的URL触发时,本应返回数据,但是目前的处理却是添加了一个中间件到Koa中,所以任何请求都会将该模版文件作为数据来返回)所以@Render并不能适用于Koa驱动。
不过我已经提交了PR了,跑通了测试用例,坐等被合并代码,但是这是一个临时的修改方案,涉及到这个库针对外部中间件注册的顺序问题,所以对于app.ts还要有额外的修改才能够实现。
// app.ts 的修改 import "reflect-metadata" import Koa from "koa" import koaViews from "koa-views" import { useKoaServer } from "routing-controllers" import { distPath } from "./config" // 手动创建koa实例,然后添加`render`的中间件,确保`ctx.render`方法会在请求的头部就被添加进去 const koa = new Koa() koa.use(koaViews(path.join(__dirname, "../views"), { options: { ext: "ejs", }, extension: "ejs", })) // 使用`useKoaServer`而不是`createKoaServer` const app = useKoaServer(koa, { controllers: [`${__dirname}/controllers/**/*{.js,.ts}`], }) // 后续的逻辑就都一样了 export default app
当然,这个是新版发出以后的逻辑了,基于现有的结构也可以绕过去,但是就不能使用@Render装饰器了,抛开koa-views直接使用内部的consolidate:
// controller/index.ts // 这个修改不需要改动`app.ts`,可以直接使用`createKoaServer` import { Get, Controller, } from "routing-controllers" import cons from "consolidate" import path from "path" @Controller() export default class { @Get("/") async router() { // 直接在接口返回时获取模版渲染后的数据 return cons.ejs(path.resolve(__dirname, "../../views/index.ejs"), { title: "Example For TypeScript React App", }) } }
目前的示例代码采用的上边的方案
小结至此,一个完整的TS前后端项目架构就已经搭建完成了(剩下的任务就是往骨架里边填代码了)。
我已经更新了之前的typescript-exmaple 在里边添加了本次重构所使用的一些前端TS+React的示例,还包括针对@Render的一些兼容。
TypeScript是一个很棒的想法,解决了N多javaScript种令人诟病的问题。
使用静态语言来进行开发不仅能够提高开发的效率,同时还能降低错误出现的几率。
结合着强大的vs code,Enjoy it.
如果在使用TS的过程中有什么问题、或者有什么更好的想法,欢迎来沟通讨论。
One more thingsBlued前端/Node团队招人。。初中高都有HC
坐标帝都朝阳双井,有兴趣的请联系我:
wechat: github_jiasm
mail: jiashunming@blued.com
欢迎砸简历
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/97066.html
摘要:怎么影响了我的思考方式对前端开发者来说,能强化了面向接口编程这一理念。使用的过程就是在加深理解的过程,确实面向接口编程天然和静态类型更为亲密。 电影《降临》中有一个观点,语言会影响人的思维方式,对于前端工程师来说,使用 typescript 开发无疑就是在尝试换一种思维方式做事情。 其实直到最近,我才开始系统的学习 typescript ,前后大概花了一个月左右的时间。在这之前,我也在...
摘要:怎么影响了我的思考方式对前端开发者来说,能强化了面向接口编程这一理念。使用的过程就是在加深理解的过程,确实面向接口编程天然和静态类型更为亲密。摘要: 学会TS思考方式。 原文:TypeScript - 一种思维方式 作者:zhangwang Fundebug经授权转载,版权归原作者所有。 电影《降临》中有一个观点,语言会影响人的思维方式,对于前端工程师来说,使用 typescript 开...
摘要:使用官方的的另外一种版本和一起使用自动配置了一个项目支持。需要的依赖都在文件中。带静态类型检验,现在的第三方包基本上源码都是,方便查看调试。大型项目首选和结合,代码调试维护起来极其方便。 showImg(https://segmentfault.com/img/bVbrTKz?w=1400&h=930); 阿特伍德定律,指的是any application that can be wr...
阅读 2025·2023-04-25 14:50
阅读 2906·2021-11-17 09:33
阅读 2609·2019-08-30 13:07
阅读 2837·2019-08-29 16:57
阅读 904·2019-08-29 15:26
阅读 3539·2019-08-29 13:08
阅读 1989·2019-08-29 12:32
阅读 3381·2019-08-26 13:57