node工具化对前端开发提效的实践

自我介绍


  • Name 刘放
  • Web前端开发@有道云课堂
  • Github brizer
  • Wechat brizer1992

分享大纲

  • Node背景介绍

  • 脚手架

  • 多工程管理

  • Mock

  • 活动页面生成系统

Node背景介绍

Node到底是什么?

Node.js® is a JavaScript runtime built on Chrome's V8 JavaScript engine.

Node的组成

  • Application

    你写的node应用或者说模块

  • Modules

    Node.js 标准库,对外提供的 JavaScript 接口,例如模块 http、buffer、fs、stream 等

  • C++ Binding

    JavaScript 与 C++ 连接的桥梁,对下层模块进行封装,向上层提供基础的 API 接口如zlib、OpenSSL、c-ares、http-parser 等

  • Addons

    JavaScript 与 C++ 连接的桥梁,第三方胶水层代码

Node的组成

  • V8

    Google 开源的高性能 JavaScript 引擎,以 C++ 实现。这也是集成在 Chrome 中的 JS 引擎

  • libuv

    提供异步功能的 C 库。它在运行时负责一个事件循环(Event Loop)、一个线程池、文件系统 I/O、DNS 相关和网络 I/O,以及一些其他重要功能。 每个操作系统对于事件多路复用器有其自身的接口,Linux是epoll,Mac OSX是kqueue,Windows的IOCP API

Node的应用场景

  • 跨平台: 传统的PC Web端,以及PC客户端 nw.js/electron 、移动端 cordova、HTML5、react-native、weex,硬件 ruff.io...
  • 后端应用开发: 网站、Api、RPC服务,比如Express/Koa/Egg/Nest/Hapi/Restify/Socket.io...
  • 前端基建: 三大框架 React/Vue/Angular 辅助开发,以及工程化演进如grunt/gulp/webpack/rollup/parcel/snowpack/rollup...
  • 命令行工具: npm上各种模块如commander/yargs/shelljs/lerna/http-server/node-http-proxy...

Node的生态优势

数据源引自modulecounts

更多Node使用场景及生态请参考

awesome-node

awesome-url

分享大纲

  • Node背景介绍

  • 脚手架

  • 多工程管理

  • Mock

  • 活动页面生成系统

脚手架

一个标准的脚手架思路

  • 命令注册: commander/yargs...
  • git拉取: download-git-repo/@gitbeaker...
  • 交互: inquirer/ora...
  • 模板: Handlebars/EJS/Jade...

从头自建脚手架


  • 重复造轮子!
  • 劳民伤财!
  • 稳定性不可保障,维护成本高
  • 耦合设计,拓展性不高

合理利用生态优势,拥抱开源

使用场景定制方式
yeoman初始化工程定制化generator,插件使用
plop工程内新增或修改文件定制交互prompts及操作actions

脚手架1.0

基于plop,工程内定制

工程内置模板及配置

img

自定义交互及行为

module.exports = {
    prompts: [{//...各种不同类型的交互
            type: "list",
            message: "请选择新增的页面位于哪个一级路由下:",
            name: "scope",
            choices: pageList
        },{
            type: "input",
            name: "chineseName",
            message: "请输入页面中文名称(例如开屏配置):"
        }],
    actions: data => {//获取交互中的得到的参数
        const { scope, name, chineseName } = data;
        return [{//...各种不同类型的行为
                type: "add",
                path: `src/javascript/admin/module/${scope}/${name}.js`,
                templateFile: "plop-templates/page/module.hbs",
                data: {umiName}
            },{
                type: "modify",
                path: `src/javascript/admin/config/admin.js`,
                templateFile: "plop-templates/page/config.hbs",
                pattern: /var exports = \{/,
                data: {name}
            }];
    }
};

效果展示

img

遗留问题


  • 模板在工程内维护,那么跨工程的通用模板不好复用

  • 每次新增都需要改到配置文件,可维护性不怎么好

脚手架2.0

基于node-plop,分离模板库和脚手架

整体思路

物料库风格

├── blocks - 各个物料本身的模板
|  ├── bower
|  |  ├── bower.hbs
|  |  └── bowerrc.hbs
|  ├── example
|  |  └── demo.hbs
|  ├── mocker
|  |  ├── dev.hbs
|  |  ├── httpmockrc.hbs
|  |  └── package.hbs
|  ├── roll
|  |  ├── dev.hbs
|  |  └── package.hbs
|  └── singleVue
|     └── index.hbs
└── plop-templates - 各个物料自己的命令行交互及具体文件流操作
   ├── bowerGenerator.js
   ├── exampleGenerator.js
   ├── mockerGenerator.js
   ├── rollGenerator.js
   └── singleVueGenerator.js

遗留问题(todo)


  • 物料不能做到本地开发,调试不便(打算通过cli强化物料开发流程)

  • 工程内需要到指定文件路径执行命令(打算通过vscode扩展或直接配上ui解决)

分享大纲

  • Node背景介绍

  • 脚手架

  • 多工程管理

  • Mock

  • 活动页面生成系统

多工程管理

工程数量

10+

组件数量

60+

WTF

每次预发上线都头大?

合理利用生态优势,拥抱开源

特点特点场景
lernamono-repo同一git仓库下,管理多个包,包含开发及发布完整流程util库及组件库
metamulti-repo同一文件结构下,管理多个不同仓库,支持插件扩展子命令,管理git等工程群
multi-repo-gitmulti-repo通过全局配置文件,跨文件目录管理不同仓库,定制化git及包依赖,不支持插件工程群

效果展示

img

核心原理


//透传参数,子进程指定路径执行
child_process.exec(command,{
    cwd:'配置文件读取的工程路径'
})

分享大纲

  • Node背景介绍

  • 脚手架

  • 多工程管理

  • Mock

  • 活动页面生成系统

Mock

哪些场景需要Mock


  • 前端本地开发时,对依赖的后端接口的Mock
  • 服务A开发时,对依赖的服务B的Mock
  • 单元测试时,对某些模块或方法的Mock

接口Mock选型

特点描述使用场景
yapi重型平台,功能齐全统一管理所有工程接口
rap2-delos重型平台,阿里妈妈出品统一管理所有工程接口
nei重型平台,网易自研统一管理所有工程接口
http-mocker笔者自研,轻量级组件内或util等example对应mock

轻量级场景

你是直接代码侵入式修改mock?

const actions = {
    save: isLocal? '/mock/success.json':`/p/train/config/submit.do?courseId=${g.courseId}`,
    detail: (isLocal?'/mock':'') + '/j/train/config/init.json',
}

export default {
    save(data):Promise<boolean>{
        console.warn(data)
        return baseSvs[isLocal?'get':'post'](actions.save, data)
    },
    detail(courseId):Promise<any>{
        return baseSvs.get(actions.detail+'?courseId='+courseId)
    },
}

还是启动一个mock服务,来响应所有接口?

import Mock from 'mockjs'

export default [
  // mock get all routes form server
  {
    url: '/article/pv',
    type: 'get',
    response: _ => {
      return {
        code: 20000,
        data: {
          pvData: [
            { key: 'PC', pv: 1024 },
            { key: 'mobile', pv: 1024 },
            { key: 'ios', pv: 1024 },
            { key: 'android', pv: 1024 }
          ]
        }
      }
    }
  },
]
  • 侵入代码影响性能和可读性!

  • 额外启动一个服务很繁琐!

  • 无法动态切换某一个接口本地和远程模式!

  • 每个规则都需要手写js,麻烦!

解决方案 Http-mocker

如何不启动服务、不侵入也可以进行mock?

直接将express或webpack-dev-server的app对象传入

const { mocker } = require('http-mockjs')
const webpackConfig = {
  devServer: {
    before:app=>{
      mocker(app)
    }
  }
}

如何识别路由,并动态切换本地或远程?

利用app.all('/*'),以path-to-regexp风格拦截路由

  app.all("/*",async (req, res, next) => {
    const proxyMatch = getMatechedRoute(proxyLists, proxyURL);
    // 匹配到对应路由
    if (proxyMatch && proxyMatch.ignore !== true) {
      // 延时返回功能
      if(proxyMatch.delay && typeof proxyMatch.delay === 'number'){
        await sleep(proxyMatch.delay)
      }
      // 校验入参格式
      if(proxyMatch.validate && !isEmptyObject(proxyMatch.validate)){
        //...
      }
      let responseBody;
      // 同时支持js模式,沙盒运行
      if(/js$/ig.test(curPath) ){
        responseBody = vm.run(jsContent)(req);
      }else{
        // json模式直接读取返回值
        responseBody = fs.readFileSync(curPath, "utf-8");
      }
      // 支持mockjs来灵活化数据
      const result = mock.mock(responseBody);
      // 自定义响应头 
      res.set(responseHeaders);
      res.send(result);
      res.end();
    }
  });

如何优化配置的便利性?

抽出独立的ui包,可视化编辑

分享大纲

  • Node背景介绍

  • 脚手架

  • 多工程管理

  • Mock

  • 活动页面生成系统

活动页面生成系统

没有开发介入的情况下,运营自己完成页面的整体搭建

应用场景:

完成页面数量

9700+

独立模块数量

120+

整体架构

模块元数据

{
    "chineseName": "移动端课程卡片带切换(抢课,拼团,提醒,收藏)",
    "content": "<ux-cms-course-list-with-tab tabList={list}></ux-cms-course-list-with-tab>",
    "dataTemplate": "c2_courselist_tab",
    "dependence": "pool/component-cms/src/course-list-with-tab/wap/ui",
    "description": "移动端课程卡片带切换(抢课,拼团,提醒,收藏)",
    "name": "m_course_list_with_tab_all_m",
    "previewImg": "http://edu-image.nosdn.127.net/367ae591-32f3-4337-a195-cafd5543082e.png",
    "style": "",
    "type": 2,
    "viewType": 2
}

数据元数据

{
  "name" : "c2_voteList",
  "description" : "投票列表模板",
  "items" : [
    {
      "description" : "投票列表类型,10为机构,20为讲师",
      "type" : "Number",
      "propertyName" : "id"
    },{
      "description" : "一页数量",
      "type" : "Number",
      "propertyName" : "nums"
    },{
      "description" : "投票结束时间",
      "type" : "Time",
      "propertyName" : "endTime"
    }
  ]
}

降低重复性工作

好好度个假

img