美文网首页小程序开放
koa2项目框架搭建

koa2项目框架搭建

作者: 宫若石 | 来源:发表于2017-11-28 21:52 被阅读0次

    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

    相关文章

      网友评论

        本文标题:koa2项目框架搭建

        本文链接:https://www.haomeiwen.com/subject/dcqebxtx.html