回想之前写的koa工程化基础项目还是在三年前,那会使用的还是webpack4和koa2.13,现在webpack已经升级到了5,发生了比较大的更新。其中许多插件也更新了,为了适用新的项目,进行一次简单的升级和重构。为了能以后能直接使用,这次加入了脚手架的功能,这样以后就可以通过简单的命令创建koa的工程化目录了。

前言

主要分三部分:

  • Koa应用的工程化搭建
  • webpack5的配置,打包压缩优化
  • 集成脚手架功能

Koa应用的工程化搭建

最小化安装测试

先安装koa并运行示例:

1
2
3
npm init -y

npm install koa

新建index.js

1
2
3
4
5
6
7
8
9
const Koa = require('koa');
const app = new Koa();

app.use(async (ctx) => {
ctx.body = 'Hello World';
});

app.listen(3000);

打开浏览器:http://localhost:3000/ ,就可以访问到Hello World。

添加路由

安装koa-router

1
npm i koa-router -S

index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const Koa = require('koa');
const Router = require('koa-router')
const app = new Koa();
const router = new Router();

router.prefix('/v1')

router.get('/', ctx => {
ctx.body = 'Hello World';
}
)
router.get('/api', (ctx) => {
ctx.body = 'Hello api';
});

app.use(router.routes()).use(router.allowedMethods());
app.listen(3000);
  • 路由前缀:router.prefix(‘api’),在路由前面加上,所有路由就要加上这个前缀

添加协议解析和跨域处理中间件

安装koa-body和@koa/cors

1
npm i koa-body @koa/cors -S

index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
const Koa = require('koa');
const Router = require('koa-router');
const { koaBody } = require('koa-body');
const cors = require('@koa/cors');
const app = new Koa();
const router = new Router();

router.get('/', (ctx) => {
ctx.body = 'Hello World';
});
router.get('/api', (ctx) => {
// 获取get参数
const params = ctx.request.query
console.log(params);
ctx.body = {
data: 'this is data ',
code: 200,
params:params
};
});
router.post('/post', (ctx) => {
// 获取post参数
let { body } = ctx.request;
console.log(body);
ctx.body = {
...body,
};
});

app.use(koaBody());
app.use(cors());
app.use(router.routes()).use(router.allowedMethods());
app.listen(3000);

添加格式JSON中间件

安装koa-json

1
npm i koa-json -S

index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
const Koa = require('koa');
const Router = require('koa-router');
const { koaBody } = require('koa-body');
const cors = require('@koa/cors');
const json = require('koa-json')
const app = new Koa();
const router = new Router();

router.get('/', (ctx) => {
ctx.body = 'Hello World';
});
router.get('/api', (ctx) => {
// 获取get参数
const params = ctx.request.query
console.log(params);
ctx.body = {
data: 'this is data ',
code: 200,
params:params
};
});
router.post('/post', (ctx) => {
// 获取post参数
let { body } = ctx.request;
console.log(body);
ctx.body = {
...body,
};
});

app.use(koaBody());
app.use(cors());
// app.use(json({pretty:false,param:'pretty'})) //需要加上参数pretty才可以格式化
app.use(json())
app.use(router.routes()).use(router.allowedMethods());
app.listen(3000);

工程化目录

  • config这个目录放webpac的配置文件
  • public放静态文件
  • src 源码
  • src/api 是api核心处理逻辑代码
  • src/router/modules 是路由,方便处理路由的prefix前缀
  • src/router/routers.js 用来合并路由
  • src/index.js 是app的入口

路由压缩

安装koa-combine-router

1
npm i koa-combine-router -S

src/index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const Koa = require('koa');
const router = require('./router/routes')
const { koaBody } = require('koa-body');
const cors = require('@koa/cors');
const json = require('koa-json');
const app = new Koa();

// 中间件
app.use(koaBody());
app.use(cors());
app.use(json());
app.use(router());
// 监听3000端口
app.listen(3000);

src\router\routes.js

1
2
3
4
5
6
7
const combineRouters = require('koa-combine-routers');

const aboutRouter = require('./modules/aboutRouter')
const publicRouter = require('./modules/publicRouter');

// 合并理由
module.exports = combineRouters(aboutRouter, publicRouter);

src\router\modules\aboutRouter.js

1
2
3
4
5
6
7
const Router = require('koa-router');
const about = require('../../api/auoutController')
const router = new Router();

router.get('/about', about);

module.exports = router;

src\router\modules\publicRouter.js

1
2
3
4
5
6
7
8
const Router = require('koa-router');
const public = require('../../api/publicController');
const router = new Router();

router.get('/', public);

module.exports = router;

src\api\auout\Controller.js

1
2
3
4
5
6
module.exports = function (ctx) {
ctx.body = {
data: 'hello,World,this is auoutController api',
code: 200,
};
};

src\api\auout\publicController.js

1
2
3
4
5
6
module.exports = function (ctx) {
ctx.body = {
data:'hello,World',
code:200
};
}

添加header中间件

安装koa-helmet

1
npm i koa-helmet -S

src/index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const Koa = require('koa');
const router = require('./router/routes')
const { koaBody } = require('koa-body');
const cors = require('@koa/cors');
const json = require('koa-json');
const helmet = require('koa-helmet');
const app = new Koa();

// 中间件
app.use(koaBody());
app.use(cors());
app.use(json());
app.use(helmet());
app.use(router());
// 监听3000端口
app.listen(3000);

静态资源处理

安装koa-helmet

1
npm i koa-static -S

src/index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const Koa = require('koa');
const router = require('./router/routes')
const { koaBody } = require('koa-body');
const cors = require('@koa/cors');
const json = require('koa-json');
const helmet = require('koa-helmet');
const statics = require('koa-static')
const path = require('path')
const app = new Koa();

// 中间件
app.use(koaBody());
app.use(cors());
app.use(json());
app.use(helmet());
app.use(statics(path.join(__dirname, '../public')));
app.use(router());
// 监听3000端口
app.listen(3000);

热加载

安装nodemon

1
npm i nodemon -D

package.json

1
2
3
"scripts": {
"start": "nodemon src/index.js"
},

webpack配置

最小化安装测试

安装webpack的相关插件:

1
npm i webpack webpack-cli clean-webpack-plugin webpack-node-externals @babel/core @babel/node babel-loader @babel/preset-env cross-env -D
  • webpack webpack-cli 是webpack 的核心;
  • clean-webpack-plugin是webpack的插件,用来清除dist目录下文件;
  • webpack-node-externals 是 排除掉node_modules,不做处理
  • @babel/core 是babel核心,用来转义ES6的
  • @babel/node 是babel 在node环境下使用需要用到的
  • babel-loader 是webpack的loader
  • @babel/preset-env是可以支持一些新的特性
  • cross-env设置环境变量

优化配置

webpack.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const path = require('path');

const nodeExclude = require('webpack-node-externals')
const webpackconfig = {
mode: 'development', //开发模式
target: 'node', //运行环境
entry: {
server: path.join(__dirname, 'src/index.js'),
},
output: {
path: path.resolve(__dirname, 'dist'), //输出的路径,需要绝对路径,
filename: `[name].bundle.js`, //输出的名字
},
devtool: 'eval-source-map', //开启source-map
module: {
rules: [
// 使用babel-loader,支持ES6语法,排除node_modules文件
{
test: /\.(js|jsx)$/,
use: {
loader: 'babel-loader',
},
exclude: [path.join(__dirname, './node_modules')], //排除node_modules
},
],
},
externals: [nodeExclude()], //排除node_modules
plugins: [new CleanWebpackPlugin()], //排除清除webpack打包文件
};

module.exports = webpackconfig;

.babelrc

1
2
3
4
5
6
7
8
9
10
11
12
{
"presets": [
[
"@babel/preset-env",
{
"targets": {
"node": "current"
}
}
]
]
}

package.json

1
2
3
4
5
"scripts": {
"start:es5": "nodemon src/index.js",
"start": "nodemon --exec babel-node src/index.js",
"bulid": "webpack"
},

优化配置2.0

单有一个Webpack打包还不行,线上和本地运行的环境不同,打包应该也不一样,所以这里需要区分线上和本地运行的环境

config文件夹下创建四个文件:

安装webpack merge:合并webpack

安装terser-webpack-plugin,压缩代码

1
npm i webpack-merge terser-webpack-plugin -D

config\webpack.config.base.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const path = require('path');
const webpack = require('webpack');
const nodeExclude = require('webpack-node-externals');
const utils = require('./utils');
const webpackconfig = {
target: 'node', //运行环境
entry: {
server: path.join(utils.APP_PATH, 'index.js'),
},
output: {
path: utils.DIST_PATH, //输出的路径,需要绝对路径,
filename: `[name].bundle.js`, //输出的名字
},
// 使用webpack5之后,webpack会从配置文件的mode中自动为process.env.NODE_ENV赋值,而取的值,就是该配置文件的mode属性。如果没有值,则会默认返回“production”。
// 让webpack不会自动读取配置文件中的mode给process.env.NODE_ENV赋值。
// 这样process.env.NODE_ENV就只是被我们自定义的文件赋值,就不会冲突了。
optimization: {
nodeEnv: false,
},
module: {
rules: [
// 使用babel-loader,支持ES6语法,排除node_modules文件
{
test: /\.(js|jsx)$/,
use: {
loader: 'babel-loader',
},
exclude: [path.join(__dirname, './node_modules')], //排除node_modules
},
],
},
externals: [nodeExclude()], //排除node_modules
plugins: [
new CleanWebpackPlugin(), //排除清除webpack打包文件
//new webpack.EnvironmentPlugin(['NODE_ENV']), // 可以直接使用 environmentPlugin
new webpack.DefinePlugin({
'process.env': {
NODE_ENV:
process.env.NODE_ENV === 'production' ||
process.env.NODE_ENV === 'prod'
? "'production'"
: "'development'",
},
}),
],
};

module.exports = webpackconfig;

config\webpack.config.dev.js

1
2
3
4
5
6
7
8
9
const webpackMerge = require('webpack-merge')
const baseWbpackConfig = require('./webpack.config.base')
const webpackConfig = webpackMerge.merge(baseWbpackConfig, {
devtool: 'eval-source-map', //开启source-map
mode: 'development', //开发模式
stats:{children:false}
});

module.exports = webpackConfig;

config\webpack.config.prod.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
const webpackMerge = require('webpack-merge');
const baseWbpackConfig = require('./webpack.config.base');
const TerserPlugin = require('terser-webpack-plugin');
const webpackConfig = webpackMerge.merge(baseWbpackConfig, {
mode: 'production', //生产模式
stats: { children: false },
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
terserOptions: {
warnings: false,
compress: {
warnings: false,
drop_console: false,
dead_code: true,
drop_debugger: true,
},
output: {
comments: false,
beautify: false,
},
mangle: true,
},
parallel: true,
}),
],
// 用来避免他们之间的重复依赖
splitChunks: {
cacheGroups: {
commons: {
name: 'commons',
chunks: 'initial',
minChunks: 3,
enforce: true,
},
},
},
},
});

module.exports = webpackConfig;

config\utils.js

1
2
3
4
5
6
7
const path = require('path');

exports.resolve = function resolve(dir) {
return path.join(__dirname, '..', dir);
};
exports.APP_PATH = exports.resolve('src');
exports.DIST_PATH = exports.resolve('dist');

优化配置3.0

webpack优化完之后,还需要优化koa应用。

整合中间件:

安装koa-compose

1
npm i koa-compose -S

src\index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import Koa from 'koa'
import router from './router/routes';
import {koaBody }from 'koa-body'
import cors from '@koa/cors'
import json from 'koa-json'
import helmet from 'koa-helmet'
import statics from 'koa-static'
import path from 'path'
import compose from 'koa-compose';

const app = new Koa();

// compose集合中间件
const middleware = compose([
koaBody(),
cors(),
json(),
helmet(),
statics(path.join(__dirname, '../public')),
]);
app.use(middleware);
app.use(router());
// 监听3000端口
app.listen(3000);

生产模式下压缩中间件:

安装koa-compress

1
npm i koa-compress -S

使用:src\index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import Koa from 'koa'
import router from './router/routes';
import {koaBody }from 'koa-body'
import cors from '@koa/cors'
import json from 'koa-json'
import helmet from 'koa-helmet'
import statics from 'koa-static'
import path from 'path'
import compose from 'koa-compose';
import compress from 'koa-compress'
const app = new Koa();

// 判断是否为开发模式,是则true,否则false
const isDevMode = process.env.NODE_ENV === 'production' ? false : true

// compose集合中间件
const middleware = compose([
koaBody(),
cors(),
json(),
helmet(),
statics(path.join(__dirname, '../public')),
]);

app.use(middleware);
app.use(router());


// 开发模式为3000端口,生产模式为12006
const port = !isDevMode ? '12006' : '3000'
// 生产模式压缩中间件
if (!isDevMode) {
app.use(compress())
}
// 监听3000端口
app.listen(port, () => {
console.log(`The server is runing at: ${port}`);
});

集成脚手架

最小化安装测试

再开始之前,不急着在原本的项目大动修改,先最小化测试一下可行性。

初始化项目:

1
npm init -y

接下来下载几个依赖包:

1
npm i [email protected] commander download-git-repo inquirer [email protected] -S

chalk: 修改控制台输出内容样式,在这里可以发挥一下你的艺术细菌了~ 开源地址:https://github.com/chalk/chalk

commander:用来编写指令和处理命令行的,类比我们用过的vue init

inquirer:一个用来设计交互式命令行的工具,非常强大,类比我们在进行完vue init后他是不是会问你用不用ts啊,eslint,CSS预处理器等等,就是它完成的

ora: 就是为了美观,下载的时候会有转圈特效。开源地址:https://github.com/sindresorhus/ora

  1. 新建一个bin文件夹,在bin文件夹下新建一个文件index.js,这个文件夹就是我们脚手架的入口文件,我们可以尝试写几句代码执行一下:
1
2
#!/usr/bin/env node
console.log('hello')

运行node ./bin/index.js,正常可以看到hello的字样

#!/usr/bin/env node,它的作用是当 系统看到这行时,能够沿着该路径查找node并执行,主要是为了兼容mac电脑,确保执行

完善配置

开始完善配置之前,梳理一下设计需求:

  • 我们只需要用户自定义项目名称,通过一句命令就可以初始化项目。

    1
    WY init koa project-name

根据这个需求,我们需要能全局运行WY,在之前的基础上修改package.json文件

package.json:

1
2
3
4
"bin": {
"WY": "bin/index.js",
"WY-init": "bin/index-init.js"
},

添加之后,使用npm link绑定命令,现在直接使用WY之后就可以输出hello,这里直接贴出index.js和index-init.js的代码:

index.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
#!/usr/bin/env node
const { program } = require('commander');
// 定义当前版本
// 定义使用方法
// 定义指令
program
.version(require('../package').version)
.usage('<command> [options]')
.command('init', 'generate a new project from a template')

// 解析命令行参数
program.parse(process.argv)

index-init.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#!/usr/bin/env node
console.log('Hello this is Koa template');

const { program } = require('commander');
const chalk = require('chalk')
const ora = require('ora')
const download = require('download-git-repo')
const tplObj = require(`${__dirname}/../template`)

program
.usage('<template-name> [project-name]')
program.parse(process.argv)
// 当没有输入参数的时候给个提示
if (program.args.length < 1) return program.help()

// 好比 vue init webpack project-name 的命令一样,第一个参数是 webpack,第二个参数是 project-name
let templateName = program.args[0]
let projectName = program.args[1]
// 小小校验一下参数
if (!tplObj[templateName]) {
console.log(chalk.red('\n Template does not exit! \n '))
return
}
if (!projectName) {
console.log(chalk.red('\n Project should not be empty! \n '))
return
}

url = tplObj[templateName]

console.log(chalk.white('\n 开始初始化项目... \n'))
// 出现加载图标
const spinner = ora("Downloading...");
spinner.start();
// 执行下载方法并传入参数
download(
url,
projectName,
err => {
if (err) {
spinner.fail();
console.log(chalk.red(`Generation failed. ${err}`))
return
}
// 结束加载图标
spinner.succeed();
console.log(chalk.cyan('\n 初始化完成!'))
console.log(chalk.cyan('\n 让我们运行项目!'))
console.log(chalk.cyan(`\n cd ${projectName}`))
console.log(chalk.cyan('\n npm install '));
console.log(chalk.cyan('\n npm run dev '));
}
)
  • 主要使用download-git-repo去拉取我们仓库的代码,引入template.json的文件。

根目录创建:template.json

1
2
3
{
"koa": "moewang0321/easyTpl"
}

使用以下命令就可以创建初始化模板了;

下载cli为全局

1
npm i wuyan_koacli -g

初始化:

1
WY init koa paoject-name

本质原理

脚手架其实本质就是去拉取一个初始化的项目,那为什么不直接git clone就好,这就是脚手架去解决的问题,每次去初始化项目,都需要去创建项目目录,然后找到这个git链接,特别是git链接,非常麻烦,脚手架一句命令就可以初始化项目无需记住任何东西,如果项目有多个项目配置,脚手架更能提现出来,比如一个项目需要使用ts,脚手架只需要创建的时候选择就好,没用脚手架,就只能通过git链接分支下载安装。这就是为什么使用脚手架和脚手架的好处。

开源代码

脚手架:https://github.com/xiaopalu/wuyan_koacli

Koa模板:https://github.com/xiaopalu/koa_template