koa2項(xiàng)目框架搭建

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
自定義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
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市雹嗦,隨后出現(xiàn)的幾起案子范舀,更是在濱河造成了極大的恐慌,老刑警劉巖了罪,帶你破解...
    沈念sama閱讀 219,366評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件锭环,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡泊藕,警方通過(guò)查閱死者的電腦和手機(jī)辅辩,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,521評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)娃圆,“玉大人玫锋,你說(shuō)我怎么就攤上這事∷夏兀” “怎么了撩鹿?”我有些...
    開(kāi)封第一講書人閱讀 165,689評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)悦屏。 經(jīng)常有香客問(wèn)我节沦,道長(zhǎng)键思,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書人閱讀 58,925評(píng)論 1 295
  • 正文 為了忘掉前任甫贯,我火速辦了婚禮稚机,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘获搏。我一直安慰自己,他們只是感情好失乾,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,942評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布常熙。 她就那樣靜靜地躺著,像睡著了一般碱茁。 火紅的嫁衣襯著肌膚如雪裸卫。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書人閱讀 51,727評(píng)論 1 305
  • 那天纽竣,我揣著相機(jī)與錄音墓贿,去河邊找鬼。 笑死蜓氨,一個(gè)胖子當(dāng)著我的面吹牛聋袋,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播穴吹,決...
    沈念sama閱讀 40,447評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼幽勒,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了港令?” 一聲冷哼從身側(cè)響起啥容,我...
    開(kāi)封第一講書人閱讀 39,349評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎顷霹,沒(méi)想到半個(gè)月后咪惠,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,820評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡淋淀,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,990評(píng)論 3 337
  • 正文 我和宋清朗相戀三年遥昧,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片朵纷。...
    茶點(diǎn)故事閱讀 40,127評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡渠鸽,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出柴罐,到底是詐尸還是另有隱情徽缚,我是刑警寧澤,帶...
    沈念sama閱讀 35,812評(píng)論 5 346
  • 正文 年R本政府宣布革屠,位于F島的核電站凿试,受9級(jí)特大地震影響排宰,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜那婉,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,471評(píng)論 3 331
  • 文/蒙蒙 一板甘、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧详炬,春花似錦盐类、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 32,017評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至隐岛,卻和暖如春猫妙,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背聚凹。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 33,142評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工割坠, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人妒牙。 一個(gè)月前我還...
    沈念sama閱讀 48,388評(píng)論 3 373
  • 正文 我出身青樓彼哼,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親湘今。 傳聞我的和親對(duì)象是個(gè)殘疾皇子沪羔,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,066評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容