Koa2 + React + Webpack热加载部署方案

实习过程中接手的一个项目是以Koa2+React+Redux+Eslint+Webpack为技术栈的。
由于项目过于繁琐复杂,这里也记录下从零开始搭建一个基于这个技术栈并支持热加载的简易部署方案。

为了使用 KOA2 能够运行,必须能够使用ES7语法 async/await 能够编译,同时React中也大量使用了import export功能,这些需求都需要借用 babel 的能力。

搭建Koa2环境

新建一个my-app项目,并在项目中初始化npm

mkdir my-app && cd my-app
npm init

初始化npm后,项目中就会自动出现一个package.json文件
本地安装koa2和babel环境,Koa2需要使用ES7语法,所以node要切换7.10以上的版本。

nvm use 7
cnpm install koa --save
cnpm install babel-core babel-polyfill babel-register babel-preset-env --save-dev

这里我使用cnpm加快安装下载速度,如果没有安装cnpm的话,请先到安装cnpm.
可能你切换node版本的时候,切换后版本的node并没有安装cnpm模块。

这里解释下babel几个模块的作用:
因为Babel默认是不会转换Map,Promise等全局对象,这里就需要加载bebel-polyfill模块进行转换
babel-preset-env 是一个新的 preset模块,可以根据配置的目标运行环境(environment)自动启用需要的 babel 插件。有了babel-preset-env就不用考虑babel-preset-es2015.

  • 目前我们写 javascript 代码时,需要使用 N 个 preset,比如:babel-preset-es2015、babel-preset-es2016。es2015 可以把 ES6 代码编译为 ES5,es2016 可以把 ES2016 代码编译为 ES6。babel-preset-latest 可以编译 stage 4 进度的 ECMAScript 代码。
  • 可能每个项目中都使用了非常多的 preset,包括不必要的。例如很多浏览器支持 ES6 的 generator,如果我们使用 babel-preset-es2015 的话,generator 函数就会被编译成 ES5 代码。
    babel-preset-env 的工作方式类似 babel-preset-latest,唯一不同的就是 babel-preset-env 会根据配置的 env 只编译那些还不支持的特性。

本地安装好babel后,还需要配置babel.在当前项目中新建一个.babelrc
在.babelrc中添加如下配置,这样Koa2 项目中就可以使用ES6的语法

{
"presets": ["env"]
}

在当前项目中新建一个文件夹server,然后进入server文件夹,创建两个入口文件分别是index.js和server.js
index.js文件中的代码如下:

require('babel-register');
require('babel-polyfill');
require('./server.js');

server.js文件中的代码如下:

import Koa from 'koa';
const app = new Koa();
app.use(async (ctx, next) => {
const start = new Date();
await next();
const ms = new Date() - start;
console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
});
// response
app.use((ctx) => {
ctx.body = 'Hello Koa in app-async.js';
});
app.listen(8080);
console.log("系统启动,端口:8080");
module.exports = app;

然后全局安装nodemon自动重启node服务工具,并启动koa2服务

cnpm install nodemon -g
nodemon server/index.js

到这里为止,koa2的环境就已经搭建完成

配置git忽略项目

新建一个.gitignore文件,文件的内容如下:

.idea/
node_modules/

安装React和ReactDOM

cnpm install react react-dom --save

在当前项目目录下新建一个app文件夹,并在文件夹中新建一个main.js
main.js的代码如下:

import React from 'react';
import ReactDOM from 'react-dom';
ReactDOM.render(<div>aaa</div>, document.getElementById('root'));

在当前项目目录下,新建一个views文件夹,并在views文件夹中新建一个index.html
index.html文件内容如下:

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
</head>
<body>
<div id="root">
</div>
<script src="bundle.js"></script>
</body>
</html>

注意:上面script的src目录是正确的,因为下面koa服务器设置静态文件目录后,会自动查找到bundle.js

安装koa-static和koa-views用于设置koa的静态文件目录和模板渲染

cnpm install koa-static --save
cnpm install koa-views --save

修改当前项目下server/server.js中的内容:

import Koa from 'koa';
import views from 'koa-views';
import path from 'path';
const app = new Koa();
app.use(require('koa-static')(path.join(__dirname, '../build')));
app.use(views(path.join(__dirname, '../views'), {
extension: 'html'
}));
app.use(async (ctx, next) => {
const start = new Date();
await next();
const ms = new Date() - start;
console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
});
// response
app.use(async(ctx) => {
await ctx.render('index.html');
});
app.listen(8080);

安装配置webpack

执行如下命令安装webpack

npm i --save webpack
npm i -g webpack
npm i --save-dev babel-loader

配置webpack.config.js文件,内容如下:

const path = require('path')
module.exports = {
devtool: 'cheap-module-eval-source-map',
entry: `${__dirname}/app/main.js`,
output: {
filename: '[name].bundle.js',
path: `${__dirname}/build`,
},
module: {
rules: [
{
test: /\.js$/,
exclude: /(node_modules|bower_components)/,
use: {
loader: 'babel-loader',
options: {
presets: ['env', 'react']
}
}
}
]
}
}

配置完webpack以后,在当前项目下执行如下命令打包启动:

webpack
nodemon server/index.js

即可看到react中内容被插入到views/index.html,显示在页面上

实现热加载的功能

执行如下命令:

cnpm install webpack-dev-server --save-dev
cnpm install react-hot-loader@next --save-dev

修改webpack.config.js,内容如下:

const path = require('path')
const webpack = require('webpack')
module.exports = {
devtool: 'cheap-module-eval-source-map',
entry: [
'react-hot-loader/patch',
'webpack-dev-server/client?http://localhost:3000',
'webpack/hot/only-dev-server', //HRM更新时刷新整个页面,如果是only-dev-server是手动刷新
`${__dirname}/app/main.js`,
],
output: {
filename: '[name].bundle.js',
path: `${__dirname}/build`,
publicPath: '/build/'
// webpack-dev-server服务上的文件是相对publicPath这个路径的,用于设置热加载的服务器
},
plugins: [
new webpack.HotModuleReplacementPlugin(), // 启用 HMR
new webpack.NamedModulesPlugin(),
// prints more readable module names in the browser console on HMR updates
],
resolve: {
// 定义了解析模块路径时的配置,常用的就是extensions,可以用来指定模块的后缀,这样在引入模块时就不需要写后缀了,会自动补全
extensions: ['.js', '.jsx']
},
module: {
rules: [
{
test: /\.js$/,
exclude: /(node_modules|bower_components)/,
use: {
loader: 'babel-loader',
options: {
presets: ['env', 'react']
}
}
}
]
},
devServer: {
hot: true,
host: 'localhost',
port: 3000,
contentBase: path.resolve(__dirname, 'build')
}
}

package.json的结尾添加如下内容:

"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build-dev": "./node_modules/.bin/webpack-dev-server --config webpack.config.js --colors"
}

修改views/index.html中代码

<script src="http://localhost:3000/build/main.bundle.js"></script>

在终端输入:

npm run build-dev
nodemon server/index.js

执行后,在浏览器打开localhost:8080
然后无论你怎么修改app/main.js中的内容,浏览器刷新下,即可获得新的数据。
到这里为止,整体的项目结构图如下:

my-app
|- app
|- main.js
|- build
|- bundle.js
|- server
|- index.js
|- server.js
|- views
|- index.html
|- .babelrc
|- package.json
|- webpack.config.js
|- .eslintrc
|- .eslintignore