koa2项目框架搭建

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>
页面渲染效果

浏览器访问http://localhost:3000

登录注册功能实现

用户模型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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 201,784评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,745评论 2 378
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,702评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,229评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,245评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,376评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,798评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,471评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,655评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,485评论 2 318
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,535评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,235评论 3 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,793评论 3 304
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,863评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,096评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,654评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,233评论 2 341

推荐阅读更多精彩内容