https://chenshenhai.github.io/koa2-note/
引用:
環(huán)境準(zhǔn)備
初始化數(shù)據(jù)庫(kù)
- 安裝MySQL5.6以上版本
- 創(chuàng)建數(shù)據(jù)庫(kù)koa_demo
create database koa_demo;
- 配置項(xiàng)目config.js
const config = {
// 啟動(dòng)端口
port: 3001,
// 數(shù)據(jù)庫(kù)配置
databaset: {
DATABASE: 'koa_demo',
USERNAME: 'root',
PASSWORD: 'abc123'
PORT: '3306',
HOST: 'localhost'
}
};
module.exports = config;
啟動(dòng)腳本
// 安裝淘寶鏡像cnpm
npm install -g cnpm --registry=https://registry.npm.taobao.org
// 安裝依賴
cnpm install
// 數(shù)據(jù)庫(kù)初始化
npm run init_sql
// 編譯react.js源碼
npm run start_static
// 啟動(dòng)服務(wù)
npm run start_server
######訪問(wèn)項(xiàng)目
chrome瀏覽器訪問(wèn):http://localhost:3001/admin
####框架設(shè)計(jì)
######實(shí)現(xiàn)概要
+ koa2搭建服務(wù)
+ MySQL作為數(shù)據(jù)庫(kù)
+ mysql 5.7版本
+ 存儲(chǔ)普通數(shù)據(jù)
+ 存儲(chǔ)session登錄態(tài)數(shù)據(jù)
+ 渲染
+ 服務(wù)端渲染:ejs作為服務(wù)端渲染的模板引擎
+ 前端渲染:用webpack2環(huán)境編譯react.js動(dòng)態(tài)渲染頁(yè)面位他,使用ant-design框架
######文件目錄設(shè)計(jì)
```javascript
├── init # 數(shù)據(jù)庫(kù)初始化目錄
│ ├── index.js # 初始化入口文件
│ ├── sql/ # sql腳本文件目錄
│ └── util/ # 工具操作目錄
├── package.json
├── config.js # 配置文件
├── server # 后端代碼目錄
│ ├── app.js # 后端服務(wù)入口文件
│ ├── codes/ # 提示語(yǔ)代碼目錄
│ ├── controllers/ # 操作層目錄
│ ├── models/ # 數(shù)據(jù)模型model層目錄
│ ├── routers/ # 路由目錄
│ ├── services/ # 業(yè)務(wù)層目錄
│ ├── utils/ # 工具類目錄
│ └── views/ # 模板目錄
└── static # 前端靜態(tài)代碼目錄
├── build/ # webpack編譯配置目錄
├── output/ # 編譯后前端代碼目錄&靜態(tài)資源前端訪問(wèn)目錄
└── src/ # 前端源代碼目錄
入口文件預(yù)覽
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存儲(chǔ)配置
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)
}));
// 配置控制臺(tái)日志中間件
app.use(convert(koaLogger()));
// 配置ctx.body解析中間件
app.use(bodyParser());
// 配置靜態(tài)資源加載中間件
app.use(convert(koaStatic(
path.join(__dirname, './../static');
)));
// 配置服務(wù)端末班渲染引擎中間件
app.use(views(path.join(__dirname, './views'), {
extension: 'ejs'
}));
// 初始化路由中間件
app.use(routers.routes()).use(routers.allowedMethods());
// 監(jiān)聽(tīng)啟動(dòng)端口
app.listen(config.port);
console.log(`the server is start at port ${config.port}`);
分層設(shè)計(jì)
后端代碼目錄
└── server
├── controllers # 操作層 執(zhí)行服務(wù)端模板渲染筛峭,json接口返回?cái)?shù)據(jù)过牙,頁(yè)面跳轉(zhuǎn)
│ ├── admin.js
│ ├── index.js
│ ├── user-info.js
│ └── work.js
├── models # 數(shù)據(jù)模型層 執(zhí)行數(shù)據(jù)操作
│ └── user-Info.js
├── routers # 路由層 控制路由
│ ├── admin.js
│ ├── api.js
│ ├── error.js
│ ├── home.js
│ ├── index.js
│ └── work.js
├── services # 業(yè)務(wù)層 實(shí)現(xiàn)數(shù)據(jù)層model到操作層controller的耦合封裝
│ └── user-info.js
└── views # 服務(wù)端模板代碼
├── admin.ejs
├── error.ejs
├── index.ejs
└── work.ejs
數(shù)據(jù)庫(kù)設(shè)計(jì)
初始化數(shù)據(jù)庫(kù)腳本
腳本目錄
./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, // 詳細(xì)信息
`create_time` varchar(20) DEFAULT NULL, // 創(chuàng)建時(shí)間
`modified_time` varchar(20) DEFAULT NULL, // 修改時(shí)間
`level` int(11) DEFAULT NULL, // 權(quán)限級(jí)別
PRIMARY KEY (`id`)
} ENGINE=InnoDB DEFAULT CHARSET=utf-8;
// 插入默認(rèn)信息
```javascript
INSERT INTO `user_info` set name='admin001', email='admin001@example.com', password='123456';
路由設(shè)計(jì)
使用koa-router中間件
路由目錄
└── server # 后端代碼目錄
└── routers
├── admin.js # /admin/* 子路由
├── api.js # resetful /api/* 子路由
├── error.js # /error/* 子路由
├── home.js # 主頁(yè)子路由
├── index.js # 子路由匯總文件
└── work.js # /work/* 子路由
子路由配置
restful API子路由
例如:api子路由/user.getUserInfo.json,整合到主路由,加載到中間件后刃唐,請(qǐng)求的路徑會(huì)是: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環(huán)境搭建
前言
由于demos/project前端渲染是通過(guò)react.js渲染的借浊,這就需要webpack2對(duì)react.js及其相關(guān)JSX及其相關(guān)ES6/7代碼進(jìn)行編譯和混淆壓縮。
webpack2
安裝和文檔
webpack2文檔可以訪問(wèn):https://webpack.js.org/
配置webpack2編譯react.js + less + sass + antd環(huán)境
文件目錄
└── static # 項(xiàng)目靜態(tài)文件目錄
├── build
│ ├── webpack.base.config.js # 基礎(chǔ)編譯腳本
│ ├── webpack.dev.config.js # 開(kāi)發(fā)環(huán)境編譯腳本
│ └── webpack.prod.config.js # 生產(chǎn)環(huán)境編譯腳本
├── output # 編譯后輸出目錄
│ ├── asset
│ ├── dist
│ └── upload
└── src # 待編譯的ES6/7号醉、JSX源代碼
├── api
├── apps
├── components
├── pages
├── texts
└── utils
webpack2編譯基礎(chǔ)配置
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: {
// 配置編譯打包規(guī)則
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'
})
]
}
};
配置開(kāi)發(fā)&生產(chǎn)環(huán)境webpack2編譯設(shè)置
為了方便編譯基本配置代碼統(tǒng)一管理反症,開(kāi)發(fā)環(huán)境(webpack.dev.config.js)和生產(chǎn)環(huán)境(webpack.prod.config,js)的編譯配置都是繼承了基本配置(webpack.base.config.js)的代碼。
開(kāi)發(fā)環(huán)境配置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');
}
})
]
});
編譯環(huán)境配置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簡(jiǎn)介
react.js是作為前端渲染的js庫(kù)(注意:不是框架)畔派。react.js使用JSX開(kāi)發(fā)來(lái)描述DOM結(jié)構(gòu)铅碍,通過(guò)編譯成virtual dom在瀏覽器中進(jìn)行view渲染和動(dòng)態(tài)交互處理。
相關(guān)文檔可查閱:https://facebook.github.io/react/
編譯使用
由于react.js開(kāi)發(fā)過(guò)程用JSX編程线椰,無(wú)法直接在瀏覽器中運(yùn)行胞谈,需要編譯成瀏覽器可識(shí)別運(yùn)行的virtual dom。目前最常用的方案是用webpack+babel進(jìn)行編譯打包憨愉。
前端待編譯源文件目錄
demos/project/static/
.
├── build # 編譯的webpack腳本
│ ├── webpack.base.config.js
│ ├── webpack.dev.config.js
│ └── webpack.prod.config.js
├── output # 輸出文件
│ ├── asset
│ ├── dist # react.js編譯后的文件目錄
│ └── ...
└── src
├── apps # 頁(yè)面react.js應(yīng)用
│ ├── 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 執(zhí)行render文件目錄
├── admin.js
├── error.js
├── index.js
└── work.js
...
react.js頁(yè)面應(yīng)用文件
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執(zhí)行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"));
靜態(tài)頁(yè)面引用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>
頁(yè)面渲染效果
瀏覽器訪問(wèn)http://localhost:3000
登錄注冊(cè)功能實(shí)現(xiàn)
用戶模型dao操作
/**
* 數(shù)據(jù)庫(kù)創(chuàng)建用戶
* @param {object} model 用戶數(shù)據(jù)模型
* @return {object} mysql執(zhí)行結(jié)果
*/
async create(model) {
let result = await dbUtils.insertData('user_info', model);
return result;
},
/**
* 查找一個(gè)存在用戶的數(shù)據(jù)
* @param {object} options 查找條件參數(shù)
* @param {object} {object|null} 查找結(jié)果
*/
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;
},
/**
* 根據(jù)用戶和密碼查找用戶
* @param {object} options 用戶名密碼對(duì)象
* @return {object|null} 查找結(jié)果
*/
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;
},
/**
* 根據(jù)用戶名查找用戶信息
* @param {string} userName 用戶賬號(hào)名稱
* @return {object|null} 查找結(jié)果
*/
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;
},
業(yè)務(wù)層操作
/**
* 創(chuàng)建用戶
* @param {object} user 用戶信息
* @return {object} 創(chuàng)建結(jié)果
*/
async create(user) {
let result = await userModel.create(user);
return result;
},
/**
* 查找存在用戶信息
* @param {object} formData 查找的表單數(shù)據(jù)
* @return {object} 查找結(jié)果
*/
async getExistOne(formData) {
let resultData = await userModel.getExistOne({
'email': formData.email,
'name': formData.userName
});
return resultData;
},
/**
* 登錄業(yè)務(wù)操作
* @param {object} formData 登錄表單信息
* @param {object} 登錄業(yè)務(wù)操作結(jié)果
*/
async signIn(formData) {
let resultData = await userModel.getOneByUserNameAndPassword({
'password': formData.password,
'name': formData.userName
});
return resultData;
},
/**
* 根據(jù)用戶名查找用戶業(yè)務(wù)操作
* @param {string} username 用戶名
* @param {object|null} 查找結(jié)果
*/
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;
},
/**
* 檢驗(yàn)用戶注冊(cè)數(shù)據(jù)
* @param {object} userInfo 用戶注冊(cè)數(shù)據(jù)
* @return {object} 校驗(yàn)結(jié)果
*/
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上下文對(duì)象
*/
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;
}
},
/**
* 注冊(cè)操作
* @param {object} ctx 上下文對(duì)象
*/
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實(shí)現(xiàn)效果
登錄模式:http://localhost:3000/admin
瀏覽器顯示登錄Tab
注冊(cè)模式:http://localhost:3001/admin
瀏覽器顯示注冊(cè)Tab
session登錄狀態(tài)判斷處理
使用session中間件
// code ...
const session = require('koa-session-minimal');
const MysqlStore = require('koa-mysql-session');
const config = require('./../config');
// code ...
const app = new Koa();
// session存儲(chǔ)配置
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 ...
登錄成功設(shè)置session到MySQL和設(shè)置sessionId到cookie
let session = ctx.session;
session.isLogin = true;
session.userName = userResult.name;
session.userId = userResult.id;
需要判斷登錄態(tài)頁(yè)面進(jìn)行session判斷
async indexPage(ctx) {
// 判斷是否有session
if(ctx.session && ctx.session.isLogin && ctx.session.userName) {
const title = 'work頁(yè)面'菱属;
await ctx.render('work', {
title
});
} else {
// 沒(méi)有登錄態(tài)則跳轉(zhuǎn)到錯(cuò)誤頁(yè)面
ctx.redirect('/error');
}
},
前言
Node 9提供了在flag模式下使用ECMAScript Modules思恐,可以讓Node編程者拋掉babel等工具的束縛挠轴,直接在Node環(huán)境下使用import/export
Node 9下import/export使用簡(jiǎn)單須知
- Node環(huán)境必須在9.0以上
- 不加loader時(shí)候铛纬,使用import/export的文件后綴名必須為.mjs(下面講利用Loader Hooks兼容.js后綴文件)
- 啟動(dòng)必須加上flag --experimental -modules
- 文件的import和export必須嚴(yán)格按照ECMAScript Modules語(yǔ)法
- ECMAScript Modules和require()的cache機(jī)制不一樣
使用簡(jiǎn)述
Node 9.x官方文檔:https://nodejs.org/dist/latest-v9.x/docs/api/esm.html
與require()區(qū)別
能力 | 描述 | require() | import |
---|---|---|---|
NODE_PATH | 從NODE_PATH加載依賴模塊 | Y | N |
cache | 緩存機(jī)制 | 可以通過(guò)require的API操作緩存 | 自己獨(dú)立的緩存機(jī)制目前不可訪問(wèn) |
path | 引用路徑 | 文件路徑 | URL格式文件路徑缕溉,例如:import A from './a?v=2017' |
extensions | 擴(kuò)展名機(jī)制 | require.extensions | Loader Hooks |
natives | 原生模塊引用 | 直接支持 | 直接支持 |
npm | npm模塊引用 | 直接支持 | 需要Loader Hooks |
file | 文件(引用) | .js鸳址,.json等直接支持 | 默認(rèn)只能是.mjs狱意,通過(guò)Loader Hooks可以自定義配置規(guī)則支持.js,*.json等Node原有支持文件 |
Loader Hooks模式使用
由于歷史原因式散,在ES6的Modules還沒(méi)確定之前括细,JavaScript的模塊化處理方案都是八仙過(guò)海伪很,各顯神通,例如前端的:AMD奋单,CMD模塊方案锉试,Node的CommonJS方案也在這個(gè)時(shí)間段誕生。等到ES6規(guī)范確定后览濒,Node中的CommonJS方案已經(jīng)是JavaScript中比較成熟的模塊化方案呆盖,單ES6怎么說(shuō)都是正統(tǒng)的規(guī)范,法理上需要兼容贷笛,所以通過(guò)以后綴.mjs這個(gè)針對(duì)ECMAScript Modules規(guī)范的Node文件方案在一片討論聲中應(yīng)運(yùn)而生应又。
當(dāng)然如果import/export只能對(duì).mjs文件起作用,意味著Node原生模塊和npm所有第三方模塊都不能起作用乏苦。所以Node 9提供了Loader Hooks株扛,開(kāi)發(fā)者可以自定義配置Resolve Hook規(guī)則去利用import/export加載使用Node原生模塊尤筐,*.js文件,npm模塊洞就,C/C++的Node編譯模塊等Node生態(tài)圈的模塊盆繁。
Loader Hooks使用步驟
- 自定義loader規(guī)則
- 啟動(dòng)的flag要加載loader規(guī)則文件
- 例如: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');
});
- 執(zhí)行代碼
node --experimental -modules --loader ./custom-loader.mjs ./index.js
- 訪問(wèn)
- 訪問(wèn) http://127.0.0.1:3000/index
- 訪問(wèn) http://127.0.0.1:3000/data
- 訪問(wèn) http://127.0.0.1:3000/todo
自定義loader規(guī)則優(yōu)化
從上面官方提供的自定義loader例子看出,只是.js文件做import/export做loader兼容旬蟋,然而我們?cè)趯?shí)際開(kāi)發(fā)中需要對(duì)npm模塊油昂,.json文件也使用import/export
loader規(guī)則優(yōu)化解析
import url from 'url';
import path from 'path';
import process from 'process';
import fs from 'fs';
// 從package.json中
// 的dependencies,devDependencies獲取項(xiàng)目所需的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);
// 項(xiàng)目所需的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'
};
}
}
規(guī)則總結(jié)
在自定義loader中,exports的resolve規(guī)則最核心的代碼是
return {
url: '',
format: ''
}
- url是模塊名稱或者文件URL格式路徑
- format是模塊格式有:esm匆浙,cjs安寺,json,builtin首尼,addon這五種模塊/文件格式我衬。
注意:目前Node對(duì)import/export的支持是:Stability: 1 - Experimental階段。后續(xù)發(fā)展有很多不確定因素饰恕。因此在還沒(méi)有去flag使用之前挠羔,盡量不要在生產(chǎn)環(huán)境中使用。
關(guān)于Node 9.x更詳細(xì)的import埋嵌、export的使用破加,可參考:
https://github.com/ChenShenhai/blog/issues/24