https://chenshenhai.github.io/koa2-note/
引用:
环境准备
初始化数据库
- 安装MySQL5.6以上版本
- 创建数据库koa_demo
create database koa_demo;
- 配置项目config.js
const config = {
// 启动端口
port: 3001,
// 数据库配置
databaset: {
DATABASE: 'koa_demo',
USERNAME: 'root',
PASSWORD: 'abc123'
PORT: '3306',
HOST: 'localhost'
}
};
module.exports = config;
启动脚本
// 安装淘宝镜像cnpm
npm install -g cnpm --registry=https://registry.npm.taobao.org
// 安装依赖
cnpm install
// 数据库初始化
npm run init_sql
// 编译react.js源码
npm run start_static
// 启动服务
npm run start_server
######访问项目
chrome浏览器访问:http://localhost:3001/admin
####框架设计
######实现概要
+ koa2搭建服务
+ MySQL作为数据库
+ mysql 5.7版本
+ 存储普通数据
+ 存储session登录态数据
+ 渲染
+ 服务端渲染:ejs作为服务端渲染的模板引擎
+ 前端渲染:用webpack2环境编译react.js动态渲染页面,使用ant-design框架
######文件目录设计
```javascript
├── init # 数据库初始化目录
│ ├── index.js # 初始化入口文件
│ ├── sql/ # sql脚本文件目录
│ └── util/ # 工具操作目录
├── package.json
├── config.js # 配置文件
├── server # 后端代码目录
│ ├── app.js # 后端服务入口文件
│ ├── codes/ # 提示语代码目录
│ ├── controllers/ # 操作层目录
│ ├── models/ # 数据模型model层目录
│ ├── routers/ # 路由目录
│ ├── services/ # 业务层目录
│ ├── utils/ # 工具类目录
│ └── views/ # 模板目录
└── static # 前端静态代码目录
├── build/ # webpack编译配置目录
├── output/ # 编译后前端代码目录&静态资源前端访问目录
└── src/ # 前端源代码目录
入口文件预览
const path = require('path');
const Koa = require('koa');
const convert = require('koa-convert');
const views = require('koa-views');
const koaStatic = require('koa-static');
const bodyParser = require('koa-bodyparser');
const koaLogger = require('koa-logger');
const session = require('koa-session-minimal');
const MysqlStore = require('koa-mysql-session');
const config = require('./../config');
const routers = require('./routers/index');
const app = new Koa();
// session存储配置
const sessionMysqlConfig = {
user: config.database.USERNAME,
password: config.database.PASSWORD,
database: config.database.DATABASE,
host: config.database.HOST
};
// 配置session中间件
app.use(session({
key: 'USER_SID',
store: new MysqlStore(sessionMysqlConfig)
}));
// 配置控制台日志中间件
app.use(convert(koaLogger()));
// 配置ctx.body解析中间件
app.use(bodyParser());
// 配置静态资源加载中间件
app.use(convert(koaStatic(
path.join(__dirname, './../static');
)));
// 配置服务端末班渲染引擎中间件
app.use(views(path.join(__dirname, './views'), {
extension: 'ejs'
}));
// 初始化路由中间件
app.use(routers.routes()).use(routers.allowedMethods());
// 监听启动端口
app.listen(config.port);
console.log(`the server is start at port ${config.port}`);
分层设计
后端代码目录
└── server
├── controllers # 操作层 执行服务端模板渲染,json接口返回数据,页面跳转
│ ├── admin.js
│ ├── index.js
│ ├── user-info.js
│ └── work.js
├── models # 数据模型层 执行数据操作
│ └── user-Info.js
├── routers # 路由层 控制路由
│ ├── admin.js
│ ├── api.js
│ ├── error.js
│ ├── home.js
│ ├── index.js
│ └── work.js
├── services # 业务层 实现数据层model到操作层controller的耦合封装
│ └── user-info.js
└── views # 服务端模板代码
├── admin.ejs
├── error.ejs
├── index.ejs
└── work.ejs
数据库设计
初始化数据库脚本
脚本目录
./demos/project/init/sql/
CREATE TABLE IF NOT EXISTS `user_info` {
`id` int(11) NOT NULL AUTO_INCREMENT, // 用户ID
`email` varchar(255) DEFAULT NULL, // 邮箱地址
`password` varchar(255) DEFAULT NULL, // 密码
`name` varchar(255) DEFAULT NULL, // 用户名
`nick` varchar(255) DEFAULT NULL, // 用户昵称
`detail_info` longtext DEFAULT NULL, // 详细信息
`create_time` varchar(20) DEFAULT NULL, // 创建时间
`modified_time` varchar(20) DEFAULT NULL, // 修改时间
`level` int(11) DEFAULT NULL, // 权限级别
PRIMARY KEY (`id`)
} ENGINE=InnoDB DEFAULT CHARSET=utf-8;
// 插入默认信息
```javascript
INSERT INTO `user_info` set name='admin001', email='admin001@example.com', password='123456';
路由设计
使用koa-router中间件
路由目录
└── server # 后端代码目录
└── routers
├── admin.js # /admin/* 子路由
├── api.js # resetful /api/* 子路由
├── error.js # /error/* 子路由
├── home.js # 主页子路由
├── index.js # 子路由汇总文件
└── work.js # /work/* 子路由
子路由配置
restful API子路由
例如:api子路由/user.getUserInfo.json,整合到主路由,加载到中间件后,请求的路径会是:http://www.example.com/api/user/getUserInfo.json
./demos/project/server/routers/api.js
/**
* restful api 子路由
*/
const router = require('koa-router');
const userInfoController = require('./../controllers/user-info');
const routers = router
.get('/user/getUserInfo.json', userInfoController.getLoginUserInfo)
.post('/user/signIn.json', userInfoController.signIn)
.post('/user.signUp.json', userInfoController.signUp);
module.exports = routers;
子路由汇总
./demos/project/server/routers/index.js
/**
* 整合所有子路由
*/
const router = require('koa-router');
const home = require('./home');
const api = require('./api');
const admin = require('./admin');
const work = require('./work');
const error = require('./error');
router.use('/', home.routes(), home.allowedMethods());
router.use('/api', api.routes(), api.allowedMethods());
router.use('/admin', admin.routes(), admin.allowedMethods());
router.use('/work', work.routes(), work.allowedMethods());
router.use('/error', error.routes(), error.allowedMethods());
module.exports = router;
app.js加载路由中间件
./demos/project/server/app.js
const routers = require('./routers/index');
// 初始化路由中间件
app.use(routers.routes()).use(routers.allowedMethods());
webpack2环境搭建
前言
由于demos/project前端渲染是通过react.js渲染的,这就需要webpack2对react.js及其相关JSX及其相关ES6/7代码进行编译和混淆压缩。
webpack2
安装和文档
webpack2文档可以访问:https://webpack.js.org/
配置webpack2编译react.js + less + sass + antd环境
文件目录
└── static # 项目静态文件目录
├── build
│ ├── webpack.base.config.js # 基础编译脚本
│ ├── webpack.dev.config.js # 开发环境编译脚本
│ └── webpack.prod.config.js # 生产环境编译脚本
├── output # 编译后输出目录
│ ├── asset
│ ├── dist
│ └── upload
└── src # 待编译的ES6/7、JSX源代码
├── api
├── apps
├── components
├── pages
├── texts
└── utils
webpack2编译基础配置
webpack.base.config.js
const webpack = require('webpack');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const path = require('path');
const sourcePath = path.join(__dirname, './static/src');
const outputPath = path.join(__dirname, './../output/dist/');
module.exports = {
// 入口文件
entry: {
'admin': './static/src/pages/admin.js',
'work': './static/src/pages/work.js',
'index': './static/src/pages/index.js',
'error': './static/src/pages/error.js'
vendor: ['react', 'react-dom', 'whatwg-fetch']
},
// 出口文件
output: {
path: outputPath,
publicPath: '/static/output/dist/',
filename: 'js/[name].js'
},
module: {
// 配置编译打包规则
rules: [
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
use: [
{
loader: 'babel-loader',
query: {
// presets: ['es2015', 'react'],
cacheDirectory: true
}
}
]
},
{
test: /\.css$/,
use: ExtractTextPlugin.extract({
fallback: "style-loader",
use: ['css-loader', 'sass-loader']
})
},
{
test: /\.less$/,
use: ExtractTextPlugin.extract({
fallback: "style-loader",
use: ['css-loader', 'less-loader']
})
},
resolve: {
extensions: ['.js', '.jsx'],
modules: [
sourcePath,
'node_modules'
]
},
plugins: [
new ExtractTextPlugin('css/[name].css'),
new webpack.optimize.CommonsChunkPlugin({
names: ['vendor'],
minChunks: Infinity,
filename: 'js/[name].js'
})
]
}
};
配置开发&生产环境webpack2编译设置
为了方便编译基本配置代码统一管理,开发环境(webpack.dev.config.js)和生产环境(webpack.prod.config,js)的编译配置都是继承了基本配置(webpack.base.config.js)的代码。
开发环境配置webpack.dev.config,js
var merge = require('webpack-merge');
var webpack = require('webpack');
var baseWebpackConfig = require('./webpack.base.config');
module.exports = merge(baseWebpackConfig, {
devtool: 'source-map',
plugins: [
new webpack.DefinePlugin({
'process.env': {
NODE_ENV: JSON.stringify('development');
}
})
]
});
编译环境配置webpack.prod.config.js
var webpack = require('webpack');
var merge = require('webpack-merge');
var baseWebpackConfig = require('./webpack.base.config');
module.exports = merge(baseWebpackConfig, {
// eval-source-map is faster for development
plugins: [
new webpack.DefinePlugin({
'process.env': {
NODE_ENV: JSON.stringify('production');
}
}),
new webpack.optimize.UglifyJsPlugin({
minimize: true,
compress: {
warning: false
}
})
]
});
使用react.js
react.js简介
react.js是作为前端渲染的js库(注意:不是框架)。react.js使用JSX开发来描述DOM结构,通过编译成virtual dom在浏览器中进行view渲染和动态交互处理。
相关文档可查阅:https://facebook.github.io/react/
编译使用
由于react.js开发过程用JSX编程,无法直接在浏览器中运行,需要编译成浏览器可识别运行的virtual dom。目前最常用的方案是用webpack+babel进行编译打包。
前端待编译源文件目录
demos/project/static/
.
├── build # 编译的webpack脚本
│ ├── webpack.base.config.js
│ ├── webpack.dev.config.js
│ └── webpack.prod.config.js
├── output # 输出文件
│ ├── asset
│ ├── dist # react.js编译后的文件目录
│ └── ...
└── src
├── apps # 页面react.js应用
│ ├── admin.jsx
│ ├── error.jsx
│ ├── index.jsx
│ └── work.jsx
├── components # jsx 模块、组件
│ ├── footer-common.jsx
│ ├── form-group.jsx
│ ├── header-nav.jsx
│ ├── sign-in-form.jsx
│ └── sign-up-form.jsx
└── pages # react.js 执行render文件目录
├── admin.js
├── error.js
├── index.js
└── work.js
...
react.js页面应用文件
static/src/apps/index.jsx文件
import React from 'react';
import ReactDOM from 'react-dom';
import {Layout, Menu, Breadcrumb} from 'antd';
import HeadeNav from './../components/header-nav.jsx';
import FooterCommon from './../components/footer-common.jsx';
import 'antd/lib/layout/style/css';
const {Header, Content, Footer} = Layout;
class App extends React.Component {
render() {
return (
<Layout className="layout">
<HeadeNav/>
<Content style={{ padding: '0 50px'}}>
<Breadcrumb style={{margin: '12px 0'}}>
<Breadcrumb.Item>Home</Breadcrumb.Item>
</Breadcrumb>
<div style={{background: '#fff', padding: 24, minHeight: 280}}>
<p>index</p>
</div>
</Content>
<FooterCommon/>
</Layout>
)
}
}
export default App;
react.js执行render渲染
static/src/pages/index.js文件
import React from 'react';
import ReactDOM from 'react-dom';
import APP from './../apps/index.jsx';
ReactDOM.render(<App />,
document.getElementById("app"));
静态页面引用react.js编译后文件
<!DOCTYPE html>
<html>
<head>
<title><%= title %></title>
<link rel="stylesheet" href="/output/dist/css/index.css">
</head>
<body>
<div id="app"></div>
<script src="/output/dist/js/vendor.js"></script>
<script src="/output/dist/js/index.js"></script>
</body>
</html>
页面渲染效果
登录注册功能实现
用户模型dao操作
/**
* 数据库创建用户
* @param {object} model 用户数据模型
* @return {object} mysql执行结果
*/
async create(model) {
let result = await dbUtils.insertData('user_info', model);
return result;
},
/**
* 查找一个存在用户的数据
* @param {object} options 查找条件参数
* @param {object} {object|null} 查找结果
*/
async getExistOne(options) {
let _sql = `
SELECT * from user_info
where email = "${options.email}" or name="${options/name}"
limit 1
`;
let result = await dbUtils.query(_sql);
if(Array.isArray(result) && result.length > 0) {
result = result[0];
} else {
result = null;
}
return result;
},
/**
* 根据用户和密码查找用户
* @param {object} options 用户名密码对象
* @return {object|null} 查找结果
*/
async getOneByUserNameAndPassword(options) {
let _sql = `
SELECT * from user_info
where password="${options/password}" and name="${options/name}"
limit 1
`;
let result = await dbUtils.query(_sql);
if(Array.isArray(result) && result.length > 0) {
result = result[0];
} else {
result = null;
}
return result;
},
/**
* 根据用户名查找用户信息
* @param {string} userName 用户账号名称
* @return {object|null} 查找结果
*/
async getUserInfoByUserName(userName) {
let result = await dbUtils.select(
'user_info',
['id', 'email', 'name', 'detial_info', 'create_time', 'modified_time', 'modified_time']
);
if(Array.isArray(result) && result.length > 0) {
result = result[0];
} else {
result = null;
}
return result;
},
业务层操作
/**
* 创建用户
* @param {object} user 用户信息
* @return {object} 创建结果
*/
async create(user) {
let result = await userModel.create(user);
return result;
},
/**
* 查找存在用户信息
* @param {object} formData 查找的表单数据
* @return {object} 查找结果
*/
async getExistOne(formData) {
let resultData = await userModel.getExistOne({
'email': formData.email,
'name': formData.userName
});
return resultData;
},
/**
* 登录业务操作
* @param {object} formData 登录表单信息
* @param {object} 登录业务操作结果
*/
async signIn(formData) {
let resultData = await userModel.getOneByUserNameAndPassword({
'password': formData.password,
'name': formData.userName
});
return resultData;
},
/**
* 根据用户名查找用户业务操作
* @param {string} username 用户名
* @param {object|null} 查找结果
*/
async getUserInfoByUserName(username) {
let resultData = await userModel.getUserInfoByUserName(userName) || {};
let userInfo = {
// id: resultData.id,
email: resultData.email,
userName: resultData.name,
detailInfo: resultData.detail_info,
createTime: resultData.create_time
}
return userInfo;
},
/**
* 检验用户注册数据
* @param {object} userInfo 用户注册数据
* @return {object} 校验结果
*/
validatorSignUp(userInfo) {
let result = {
success: false,
message: ''
};
if(/[a-z0-9\_\-]{6,16}/.test(userInfo.userName) === false) {
result.message = userCode.ERROR_USER_NAME;
return result;
}
if(!validator.isEmail(userInfo.email)) {
result.message = userCode.ERROR_EMAIL;
return result;
}
if(!/[\w+]{6,16}/.test(userInfo.password)) {
result.message = userCode.ERROR_PASSWORD
return result;
}
if(userInfo.password !== userInfo.confirmPassword) {
result.message = userCode.ERROR_PASSWORD_CONFORM;
return result;
}
result.susccess = true;
return result;
}
controller操作
/**
* 登录操作
* @param {object} ctx上下文对象
*/
async signIn(ctx) {
let formData = ctx.request.body;
let result = {
success: false,
message: '',
data: null,
code: ''
};
let userResult = await userInfoService.signIn(formData);
if(userResult) {
if(formData.userName === userResult.name) {
result.success = true;
} else {
result.message = userCode.FAIL_USER_NAME_OR_PASSWORD_ERRPR;
result.code = 'FAIL_USER_NAME_OR_PASSWORD_ERRPR';
}
} else {
result.code = 'FAIL_USER_NO_EXIST';
result.message = userCode.FAIL_USER_NO_EXIST;
}
if(formData.source === 'form' && result.success === true) {
let session = ctx.session;
session.isLogin = true;
session.userName = userResult.name;
session.userId = userResult.id;
ctx.redirect('/work');
} else {
ctx.body = result;
}
},
/**
* 注册操作
* @param {object} ctx 上下文对象
*/
async signUp(ctx) {
let formData = ctx.request.body;
let result = {
success: false,
message: '',
data: null
}
let vaildateResult = userInfoService.validatorSignUp(formData);
if(validateResult.success === false) {
result = vaildateResult;
ctx.body = result;
return;
}
let existOne = await userInfoService.getExistOne(formData);
console.log(existOne);
if(existOne) {
if(existOne.name === formData.userName) {
result.message = userCode.FAIL_USER_NAME_IS_EXIST;
ctx.body = result;
return;
}
if(exitsOne.email === formData.email) {
result.message = userCode.FAIL_EMAIL_IS_EXIST;
ctx.body = result;
return;
}
}
let userResult = await userInfoService.create({
email: formData.email,
password: formData.password,
name: formData.userName,
create_time: new Date().getTime(),
level: 1
});
console.log(userResult);
if(userResult && userResult.insertId * 1 > 0) {
result.success = true;
} else {
result.message = userCode.ERROR_SYS;
}
ctx.body = result;
}
前端用react.js实现效果
登录模式:http://localhost:3000/admin
浏览器显示登录Tab
注册模式:http://localhost:3001/admin
浏览器显示注册Tab
session登录状态判断处理
使用session中间件
// code ...
const session = require('koa-session-minimal');
const MysqlStore = require('koa-mysql-session');
const config = require('./../config');
// code ...
const app = new Koa();
// session存储配置
const sessionMysqlConfig = {
user: config.database.USERNAME,
password: config.database.PASSWORD,
database: config.database.DATABASE,
host: config.database.HOST
}
// 配置session中间件
app.use(session({
key: 'USER_SID',
store: new MysqlStore(sessionMysqlConfig);
}));
// code ...
登录成功设置session到MySQL和设置sessionId到cookie
let session = ctx.session;
session.isLogin = true;
session.userName = userResult.name;
session.userId = userResult.id;
需要判断登录态页面进行session判断
async indexPage(ctx) {
// 判断是否有session
if(ctx.session && ctx.session.isLogin && ctx.session.userName) {
const title = 'work页面';
await ctx.render('work', {
title
});
} else {
// 没有登录态则跳转到错误页面
ctx.redirect('/error');
}
},
前言
Node 9提供了在flag模式下使用ECMAScript Modules,可以让Node编程者抛掉babel等工具的束缚,直接在Node环境下使用import/export
Node 9下import/export使用简单须知
- Node环境必须在9.0以上
- 不加loader时候,使用import/export的文件后缀名必须为.mjs(下面讲利用Loader Hooks兼容.js后缀文件)
- 启动必须加上flag --experimental -modules
- 文件的import和export必须严格按照ECMAScript Modules语法
- ECMAScript Modules和require()的cache机制不一样
使用简述
Node 9.x官方文档:https://nodejs.org/dist/latest-v9.x/docs/api/esm.html
与require()区别
能力 | 描述 | require() | import |
---|---|---|---|
NODE_PATH | 从NODE_PATH加载依赖模块 | Y | N |
cache | 缓存机制 | 可以通过require的API操作缓存 | 自己独立的缓存机制目前不可访问 |
path | 引用路径 | 文件路径 | URL格式文件路径,例如:import A from './a?v=2017' |
extensions | 扩展名机制 | require.extensions | Loader Hooks |
natives | 原生模块引用 | 直接支持 | 直接支持 |
npm | npm模块引用 | 直接支持 | 需要Loader Hooks |
file | 文件(引用) | .js,.json等直接支持 | 默认只能是.mjs,通过Loader Hooks可以自定义配置规则支持.js,*.json等Node原有支持文件 |
Loader Hooks模式使用
由于历史原因,在ES6的Modules还没确定之前,JavaScript的模块化处理方案都是八仙过海,各显神通,例如前端的:AMD,CMD模块方案,Node的CommonJS方案也在这个时间段诞生。等到ES6规范确定后,Node中的CommonJS方案已经是JavaScript中比较成熟的模块化方案,单ES6怎么说都是正统的规范,法理上需要兼容,所以通过以后缀.mjs这个针对ECMAScript Modules规范的Node文件方案在一片讨论声中应运而生。
当然如果import/export只能对.mjs文件起作用,意味着Node原生模块和npm所有第三方模块都不能起作用。所以Node 9提供了Loader Hooks,开发者可以自定义配置Resolve Hook规则去利用import/export加载使用Node原生模块,*.js文件,npm模块,C/C++的Node编译模块等Node生态圈的模块。
Loader Hooks使用步骤
- 自定义loader规则
- 启动的flag要加载loader规则文件
- 例如:node --experimental -modules --loader ./custom-loader.mjs ./index.js
Koa2直接使用import/export
- 文件目录
├── esm
│ ├── README.md
│ ├── custom-loader.mjs
│ ├── index.js
│ ├── lib
│ │ ├── data.json
│ │ ├── path.js
│ │ └── render.js
│ ├── package.json
│ └── view
│ ├── index.html
│ ├── index.html
│ └── todo.html
主文件代码:
import Koa from 'koa';
import {render} from './lib/render.js';
import data from './lib/data.json';
let app = new Koa();
app.use((ctx, next) => {
let view = ctx.url.substr(1);
let content;
if(view === '') {
content = render('index');
} else if(view === 'data') {
content = data;
} else {
content = render(view);
}
ctx.body = contentl
});
app.listen(3000, ()=>{
console.log('the modules test server is starting');
});
- 执行代码
node --experimental -modules --loader ./custom-loader.mjs ./index.js
自定义loader规则优化
从上面官方提供的自定义loader例子看出,只是.js文件做import/export做loader兼容,然而我们在实际开发中需要对npm模块,.json文件也使用import/export
loader规则优化解析
import url from 'url';
import path from 'path';
import process from 'process';
import fs from 'fs';
// 从package.json中
// 的dependencies,devDependencies获取项目所需的npm模块信息
const ROOT_PATH = process.cwd();
const PKG_JSON_PATH = path.join(ROOT_PATH, 'package.json');
const PKG_JSON_STR = fs.readFileSync(PKG_JSON_PATH, 'binary');
const PKG_JSON = JSON.parse(PKG_JSON_STR);
// 项目所需的npm模块信息
const allDependencies = {
...PKG_JSON.dependencies || {},
...PKG_JSON.devDependencies || {}
}
// Node原生模信息
const builtins = new Set(
Object.keys(process.binding('natives')).filter((str) =>
/^(?!(?:internal|node|v8)\/)/.test(str)
);
);
// 文件引用兼容后缀名
const JS_EXTENSIONS = new Set(['.js', '.mjs'])
const JSON_EXTENSIONS = new Set(['.json']);
export function resolve(specifier, parentModuleURL, defaultResolve) {
// 判断是否为Node原生模块
if(builtins.has(specifier)) {
return {
url: specifier,
format: 'builtin'
};
}
// 判断是否为npm模块
if(allDependencies && typeof allDependencies[specifier] === 'string') {
return defaultResolve(specifier, parentModuleURL);
}
// 判断是否为npm模块
if(/^\.{0,2}[/]/.test(specifier) !== true && !specifier.startsWith('file:')) {
throw new Error(
`imports must begin with '/', './', or '../'; ${specifier} does not`
);
}
// 判断是否为*.js,*.mjs,*.json文件
const resolved = new url.URL(specifier, parentModuleURL);
const ext = path.extname(resolved.pathname);
if(!JS_EXTENSIONS.has(ext) && !JSON_EXTENSIONS.has(ext) {
throw new Error(
`Cannot load file with non-JavaScript file extension ${ext}`;
);
};
// 如果是*.js,*.mjs文件
if(JS_EXTENSIONS.has(ext)) {
return {
url: resolved.href,
format: 'esm'
};
}
// 如果是*.json文件
if(JSON_EXTENSIONS.has(ext)) {
return {
url: resolved.href,
format: 'json'
};
}
}
规则总结
在自定义loader中,exports的resolve规则最核心的代码是
return {
url: '',
format: ''
}
- url是模块名称或者文件URL格式路径
- format是模块格式有:esm,cjs,json,builtin,addon这五种模块/文件格式。
注意:目前Node对import/export的支持是:Stability: 1 - Experimental阶段。后续发展有很多不确定因素。因此在还没有去flag使用之前,尽量不要在生产环境中使用。
关于Node 9.x更详细的import、export的使用,可参考:
https://github.com/ChenShenhai/blog/issues/24
网友评论