TypeScript是一种开源编程语言,在软件开发社区中愈来愈受欢迎。TypeScript带来了可选的静态类型检查以及最新的ECMAScript特性。
做为Javascript的超集,它的类型系统经过在键入时报告错误来加速和保障咱们的开发,同时愈来愈多对的库或框架提供的types
文件可以让这些库/框架的API一目了然。我对这门语言垂涎已久,可是迟迟没法找到练手的地方。
很显然的,我的博客又一次的成了个人学习试验田😸。我放弃了上一版Vue单页面的框架,改成基于TypeScript/Koa的多页面应用。在改造的过程当中,我试着将服务端(Koa)代码以及前端代码都使用TypeScript来开发,中间使用了webpack做为开发时先后端的桥梁。javascript
. ├── .babelrc ├── bin │ ├── dev.server.ts │ ├── pm2.json │ └── app.ts ├── config # 配置目录 │ ├── dev.ts │ └── prod.ts ├── nodemon.json ├── package.json ├── postcss.config.js ├── scripts │ └── webpack.config.js ├── src # 源码 │ ├── assets # 静态资源 │ │ ├── imgs │ │ ├── scss │ │ └── ts │ ├── entries # webpack入口 │ │ ├── blog.ts │ │ └── index.ts │ └── views # 模板(文件名与入口一一对应) │ ├── blog.html │ ├── index.html │ └── layout # 模板布局 │ ├── footer.html │ └── header.html ├── server # 服务端 │ ├── app.ts │ └── middleware │ └── webpack-dev-middleware.ts ├── test # 单元测试 │ └── .gitkeep ├── tsconfig.front.json └── tsconfig.json
npm i --save koa koa-{router,bodyparser,static,ejs} npm i -D typescript ts-node nodemon @types/{node,koa,koa-router,koa-bodyparser}
ts-node
启动项目后,整个流程分为两部分,蓝色线条的表明纯服务端代码的编译过程。服务端代码是纯typeScript
文件,能够经过ts-node
直接编译运行。前端代码包含了ejs
渲染所须要的模板文件(html),以及模板中所引用的静态资源(ts, scss, img),这部分须要经过webpack来编译。css
// path: bin/dev.server.ts import webpack = require('webpack') // 引入项目主模块 import app from '../server/app' // webpack-dev-middleware中间件 import devMiddleware from '../server/middleware/webpack-dev-middleware' // webpack配置文件 const webpackConfig = require('../scripts/webpack.config.js') // https://webpack.docschina.org/api/compiler const compiler = webpack(webpackConfig) app.use(devMiddleware(compiler, { // 很重要,提供了静态资源的路径, 该路径与webpackConfig中的output.publicPath 对应 publicPath: '/' })) const PORT: number = Number(process.env.PORT) || 3000 app.listen(PORT)
webpack-dev-middleware
中间件来调用webpack,以便 ctx.render
时渲染的就是编译后的模板文件;glob
模块来遍历src/entries/*.ts
下的入口文件,生成webpack的entry
配置项config.entry
;这也是Webpack多页面配置必不可少的一步;ts-loader/babel-loader
等来编译入口文件以及入口文件中所引用的ts/js
模块;css-loader/sass-loader
等来编译入口文件中所引用的scss/css
模块,而且直接经过MiniCssExtractPlugin.loader
来独立生成css文件;url-loader
等来编译引用的资源文件,如image;config.entry
来查找对应的模板文件,生成多页面的HtmlWebpackPlugin
配置;经过webpack-dev-middleware
编译后的文件都在 内存中, 可是ejs
渲染所须要的模板文件都必须为真实的物理文件。所以须要有两个output
,一个将静态资源放置在内存中,一个则直接编译后生成物理文件放置在dist/views
中(方案见[ejs模板文件没法使用内存文件的解决方法]章节)。
webpack-dev-middleware 是一个封装器(wrapper),它能够把 webpack 处理过的文件发送到一个 server。
webpack-dev-middleware是一个标准的express中间件,其一个重要做用就是将通过webpack编译打包的文件生成在内存中,以便下一个中间件使用。不少Cli使用的webpack-dev-server
就是基于express+webpack-dev-middleware
的实现。html
因为webpack-dev-middleware是一个标准的express中间件,在Koa中不能直接使用它,所以须要将webpack-dev-middleware封装一下,以便Koa可以直接使用。前端
npm i -D webpack-dev-middleware @types/webpack-dev-middleware
// path: server/middleware/webpack-dev-middleware.ts // opts 配置同 webpack-dev-middleware import * as WebpackDevMiddleware from 'webpack-dev-middleware' import * as Koa from 'koa' import { NextHandleFunction } from 'connect' import webpack = require('webpack') const devMiddleware = (compiler: webpack.ICompiler, opts: WebpackDevMiddleware.Options) => { const middleware = WebpackDevMiddleware(compiler, opts) return async (ctx: Koa.Context, next: NextHandleFunction) => { await middleware(ctx.req, { // @ts-ignore end: (content:string) => { ctx.body = content }, setHeader: (name, value: any) => { ctx.set(name, value) } }, next) } } export default devMiddleware
webpack 要实现一个多页面的配置,须要配置多个入口。随着深刻的开发,入口每每是动态不定的,所以要实现一个动态获取入口的方法。java
glob是一个容许正则匹配文件路径的模块,借助glob模块,很容易遍历某个目录下的全部文件来生成一个入口的map。node
// path: scripts/webpack.config.js // ... // 获取入口文件 const entries = () => { // 经过 globa.sync 方法获取 src/entries/下的全部 .ts 文件 const entriesFile = glob.sync(path.resolve(__dirname, '../src/entries/*.ts')) /** * 入口字典 * { * index: 'src/entries/index.ts', * blog: 'src/entries/blog.ts', * // ... * } */ const map = Object.create(null) // 遍历匹配到的文件列表 for (let i = 0; i < entriesFile.length; i++) { const filePath = entriesFile[i] // 提取文件名 const match = filePath.match(/entries\/([a-zA-Z0-9-_]+)\.ts$/) // 将文件名做为 key, 存入map // 如: src/entries/index.ts , src/entries/blog.ts 将分别做为 index / blog 两个入口 map[match[1]] = filePath } return map } // webpack config const webpackConfig = { entry: entries(), // ... } module.exports = webpackConfig
因为前端源码使用的typescript/es6/scss,这些文件必须通过编译后才能被浏览器识别。同时,对资源文件的版本处理(加版本号),也须要借助HtmlWebpackPlugin
这个插件注入到对应模板上。就像流程图中示意的那样,当访问路由时(如 localhost:3000/blog),ejs 加载的并非 src/views
下的模板,而是编译后(此时 css/js的引用已经注入到页面中)的位于 dist/views
下的新的模板文件。多入口对应多个模板,每一个模板文件和入口文件应该有个映射关系,这个关系能够经过维护一个map来实现(不利于增改),也能够经过文件命名规则来实现。这里采用命名规则来实现,这样更有利于自动化。react
// path: scripts/webpack.config.js // ... // 遍历webpackConfig入口, key 对应了模板的文件名,这个命名规则能够更复杂些,好比增长对子目录的支持 // { // index: 'views/index.html', // blog: 'views/blog.html' // } const isProduction = process.env.NODE_ENV === 'production' Object.keys(webpackConfig.entry).forEach(entry => { // 在 plugins 配置中增长了多个 HtmlWebpackPlugin 实例 webpackConfig.plugins.push(new HtmlWebpackPlugin({ filename: 'views/' + entry + '.html', template: path.resolve(__dirname, `../src/views/${entry}.html`), chunks: [entry], // 将入口文件打包后的文件注入到对应的页面中 alwaysWriteToDisk: true, // 该配置项说明见 [ejs模板文件没法使用内存文件的解决方法] 章节 minify: { removeComments: isProduction, collapseWhitespace: isProduction, removeAttributeQuotes: false, minifyCSS: isProduction, minifyJS: isProduction }, })) })
webpack-dev-middleware 的一个重要特性就是生成的文件都位于内存中,是一个内存型的文件系统。而koa-ejs
做为渲染引擎只能加载真实的物理文件,当它加载 dist/vies/*.html
时会报文件未找到的错。所以,对模板文件的编译就不能再像其余资源同样生成于内存中,而是要把模板文件真真切切的生成为文件。HtmlWebpackHarddiskPlugin 这个webpack插件能够完美解决。webpack
npm i -D html-webpack-harddisk-plugin
// path: scripts/webpack.config.js // ... const HtmlWebpackHarddiskPlugin = require('html-webpack-harddisk-plugin') // ... 见 [入口文件映射模板文件] 章节 webpackConfig.plugins.push(new HtmlWebpackPlugin({ // 增长该配置项 alwaysWriteToDisk: true, })) // ... // 应用 HtmlWebpackHarddiskPlugin 插件 webpackConfig.plugins.push(new HtmlWebpackHarddiskPlugin())
Server端和前端可能在typescript的配置上有所不一样,尤为是在一些编译选项上。此时须要两个不一样的配置文件。tsconfig.json
是默认的TypeScript配置文件, 这里就做为Server端的配置项,根目录新建 tsconfig.front.json
做为前端的配置文件:git
// ./tsconfig.front.json { "compilerOptions": { "outDir": "./dist/", "noImplicitAny": true, "module": "es6", "target": "es5", "jsx": "react", "allowJs": true }, "include":[ "src/assets/**/*", "src/entries/**/*" ], "exclude": [ "node_modules" ] }
同时,须要在webpack配置文件中指定配置文件路径:es6
// path: scripts/webpack.config.js // ... webpackConfig.module = { rules: [{ test: /\.tsx?$/, include: [ path.resolve(__dirname, '../src/') ], use: [{ loader: 'ts-loader', options: { // 指定配置文件 configFile: '../tsconfig.front.json' } }], }, // ... ], }, // ...
至此,基于基于WEBPACK/TYPESCRIPT/KOA的先后端多页面开发环境配置完毕。配置nodemon, nodemon将监视启动目录中的文件,若是有任何文件更改,nodemon将自动从新启动node应用程序。
运行npm start
, 其实是运行nodemon
, nodemon将根据nodemon.json
配置项来启动npm run dev
命名。当src目录下的文件有任何变化时,它将重启应用程序。
// ./nodemon.json { "watch": ["src", "server"], "exec": "npm run dev", "ext": "ts" }
在package.json
的scripts
中加入运行脚本方便一键启动。
// ./package.json { "scripts": { "start": "nodemon", "dev": "rm -rf dist && cross-env NODE_ENV=development ts-node bin/dev.server.ts", } }
相对而言,生产环境的配置就简单多了。当运行npm run build
时,仍是分两步走;
tsc
命令将 server 下的服务端代码所有编译到 dist/server
目录;webpack
命令将 src 下的前端代码所有编译到 dist/*
相应目录;当经过 pm2 restart ./bin/pm2.json
或者 node ./bin/app.js
(须要设置环境变量为production
) 启动服务时,实际上已经运行的是编译后的代码。这里须要注意两点:
static
目录指向了 dist/static
views
目录指向了 dist/views
// ./server/app.ts // 获取环境变量 const env = process.env.NODE_ENV || 'development' const isDev = env === 'development' require('koa-ejs')(app, { // root 为通过webpack编译后的真实模板路径 // 生产环境下,server已经在dist目录,修改以下: root: path.resolve(__dirname, isDev ? '../dist/views' : '../views'), })
// ./bin/app.js // 引用了编译后的 app.js 主文件 const app = require('../dist/server/app') const path = require('path') // 设置静态资源目录 app.use(require('koa-static')(path.resolve(__dirname, '../dist')))
此时,dist目录结构以下:
. ├── server │ ├── app.js │ └── middleware │ └── webpack-dev-middleware.js ├── static │ ├── css │ │ ├── blog.4dcddae.css │ │ └── index.4dcddae.css │ └── js │ ├── blog.4dcddae.js │ └── index.4dcddae.js └── views ├── blog.html └── index.html
至此,基于webpack/koa/typescript的多页面服务端渲染的项目以及开发和生产环境的配置已经搭建完毕。其中webpack-dev-middleware
在开发环境中提供了桥梁的做用。TypeScript做为JavaScript的超集,不只能够有效杜绝由变量类型引发的误用问题,并且经过@types
和如vscode
等编辑器的配合,能够更方便快速的让开发者了解一些库/框架的API。