河马嘴不大

河马嘴不大 查看完整档案

北京编辑  |  填写毕业院校阿里巴巴  |  底层员工 编辑 www.xiejingpeng.cn 编辑
编辑

生命比你想象的要短,多写两行代码吧!少年

个人动态

河马嘴不大 发布了文章 · 3月9日

如何优雅的打包工具库

写在前面的话

日常工作中,为了提高开发能效,我们通常需要抽象整合一些代码,例如:工具库,组件库。这些库不同于我们的业务开发项目,一般形成 npm 包的形式发布,给别人调用,所以需要不同的方式去编译和打包。

需要打出什么?

结合实际生产环境的需要一般的使用场景有以下几种:

  • CommonJS 格式:提供给 WebpackBrowserify, 或者 Node 环境,SSR,Jest等
  • ES2015 的格式:直接提供模块化的方案,方便 Tree shaking
  • UMD 格式:各种兼容
  • <script> 标签注入挂载到浏览器 window 对象下,或者是 Node 的 global 下

社区的例子

Redux 的打包输出
image.png

  • dist:UMD格式,兼容 window 或 global ,同时也兼容 AMD、CommonJS格式
  • es:ES2015 的风格的模块
  • lib:CommonJS 格式
  • src:源代码

再看一下它打包的输出配置:

// package.json
...
  "main": "lib/redux.js",
  "unpkg": "dist/redux.js",
  "module": "es/redux.js",
  "types": "types/index.d.ts",
  "files": [
    "dist",
    "lib",
    "es",
    "src",
    "types"
  ],
...

现有的社区工具

  • Webpack
  • Rollup
  • Babel
  • Father(没错,就是这个名字。。。其实是 umi-library 的别名)

用什么工具?

Webpack、Rollup、Babel大家都比较熟悉,就不一一赘述了,对于工具库的打包,社区一般都是推荐 Rollup,Redux 也是用 Rollup 打包,但配置的有点多,看了下源码,大概用了 200+ 行实现。这里主要讲一下 Father ,基于 Babel 和 Rollup 的封装。

这是官网的特性:

  • ✔︎ 基于 docz 的文档功能
  • ✔︎ 基于 rollup 和 babel 的组件打包功能
  • ✔︎ 支持 TypeScript
  • ✔︎ 支持 cjs、esm 和 umd 三种格式的打包
  • ✔︎ esm 支持生成 mjs,直接为浏览器使用
  • ✔︎ 支持用 babel 或 rollup 打包 cjs 和 esm
  • ✔︎ 支持多 entry
  • ✔︎ 支持 lerna
  • ✔︎ 支持 css 和 less,支持开启 css modules
  • ✔︎ 支持 test
  • ✔︎ 支持用 prettier 和 eslint 做 pre-commit 检查

实践

在项目中用一下,看看有木有文档说的那么香

需要编译的源文件:

// src/index.js
const sum = (a, b) => a + b;
export default sum;

配置一下文件 .fatherrc.js

// .fatherrc.js
export default {
  entry: 'src/index.js',
  esm: { type: 'rollup', file: 'es/index' },
  cjs: { type: 'rollup', file: 'lib/index'},
  umd: { file: 'umd/index', name: 'father-test' }
};

输出:
image.png
比较符合我们的期望,分别输出了:UMD、CommonJS、ES2015 三种规格的代码,再来看一下具体内容。

ES2015格式的编译代码:

// dist/es/index.js
var sum = function sum(a, b) {
  return a + b;
};

export default sum;

CommonJS格式的编译代码:

// dist/lib/index.js
'use strict';

var sum = function sum(a, b) {
  return a + b;
};

module.exports = sum;

UMD格式的编译代码:

// dist/umd/index.js
(function (global, factory) {
    typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
    typeof define === 'function' && define.amd ? define(factory) :
    (global = global || self, global['father-test'] = factory());
}(this, (function () { 'use strict';

    var sum = function sum(a, b) {
      return a + b;
    };

    return sum;

})));

总体来看,还是比较香的,配置很简单,适合用于工具包的打包,输出的内容多样化,能够满足不同的使用场景。

查看原文

赞 0 收藏 0 评论 1

河马嘴不大 发布了文章 · 2019-12-06

Babel 不同配置的实验报告

写在前面的话

JavaScript语言的更新,也伴随着 Babel 的成长, 对于一个前端而言,JavaScript 新的 API 自然很香,但代价就是我们要转译它,比较常用工具的就是 Babel 。

ECMA有很多版本,6、7、8、9 ....,前端浏览器也有多种,每种也有着不同的版本,为了实现这多对多的关系,Babel 也表示非常难,最后的结果就是,随着 Babel 的升级,前端同学有一堆包要学习和了解,如 @babel/cli@babel/core@babel/polyfill@babel/preset-env 等等,Babel 目前最新的版本是 7.7.0,前一次比较重大的升级是 7.4.0,本着客户第一(Babel 很香)的原则, 下面对 Babel 的配置做了一些实验,本文主要是对于 Babel 的使用,不针对工作原理。

一、实验目的

测试 Babel 的不同配置对于 JavaScript 编译结果的影响

二、实验环境和要求

依赖包版本

@babel/core 7.7.0  
@babel/cli 7.7.0
@babel/preset-env 7.7.1 
@babel/runtime 7.7.0
@babel/plugin-transform-runtime 7.7.0
@babel/corejs@3 7.7.0 

实验基础数据

创建实验基本的样本文件 index.js

const cat = 'May'
const arrow = () => `Name: ${cat}`.padStart(2);
const promise = new Promise();
let map = new Map();

我们通过对 Babel 的配置进行修改,实验在不同配置下的编译结果 。
本实验使用 babel-cli 命令行直接编译和输出文件,对应的命令如下:
npx babel index.js --out-file index_compile.js
将样本文件 index.js 编译输出到 index_compile.js
目标浏览器不配置,采用 Babel 默认,即转换所有 ECMAScript 2015+。

三、实验内容

初始化准备

首先安装实验用到的依赖包

npm install --save-dev @babel/core @babel/cli @babel/preset-env 

项目根目录下创建 babel.config.js 文件,用于配置 Babel

module.exports = {};

配置一

不进行配置

module.exports = {};

实验结果:

const cat = 'May';

const arrow = () => `Name: ${cat}`.padStart(2);

const promise = new Promise();
let map = new Map();

除了多了两行空白,没有什么其它变化,说明 Babel 是基于插件架构的,假如你什么插件也不提供,那么 Babel 什么也不会做,即你输入什么输出的依然是什么。

配置二

增加 ES+ 转换集合包 @babel/preset-env

module.exports = {
  presets: ['@babel/preset-env'],
};

实验结果:

"use strict";

var cat = 'May';

var arrow = function arrow() {
  return "Name: ".concat(cat).padStart(2);
};

var promise = new Promise();
var map = new Map();

对样本中的 const 和 let 以及箭头函数和模板字符串语法进行了处理,但对于 padStart 、PromiseMap 并没有处理,说明 @babel/preset-env 只能处理 ES+ 中新增的基本语法,不能对新增类和类的扩展属性进行处理。

这里就需要我们使用  @babel/polyfill ,实现新的内置函数、实例方法的转换。在我们使用  @babel/preset-env 的同时,它有个 useBuiltIns 选项,用来控制怎么样处理 @babel/polyfill。这里的 useBuiltIns 有三个可选属性: 'entry' | 'usage' | false,默认是 false 。

为了实现编译结果的可运行,我们需要改变样本代码,如下:

import "@babel/polyfill";

const cat = 'May';
const arrow = () => `Name: ${cat}`.padStart(2);
const promise = new Promise();
let map = new Map();

配置三

module.exports = {
  presets: [
    ['@babel/preset-env', {
      useBuiltIns: false,
    }]
  ],
};

实验结果:

"use strict";

require("@babel/polyfill");

var cat = 'May';

var arrow = function arrow() {
  return "Name: ".concat(cat).padStart(2);
};

var promise = new Promise();
var map = new Map();

实验结果同配置二,做了基本语法的转译,直接引入了 @babel/polyfill 整个包。

配置四

module.exports = {
  presets: [
    ['@babel/preset-env', {
      useBuiltIns: 'usage',
    }]
  ],
};

实验结果:

"use strict";

require("core-js/modules/web.dom.iterable");

require("core-js/modules/es6.array.iterator");

require("core-js/modules/es6.string.iterator");

require("core-js/modules/es6.map");

require("core-js/modules/es6.promise");

require("core-js/modules/es6.object.to-string");

require("core-js/modules/es7.string.pad-start");

var cat = 'May';

var arrow = function arrow() {
  return "Name: ".concat(cat).padStart(2);
};

var promise = new Promise();
var map = new Map();

同时我们得到了一些 Warning:

Warning1

WARNING: We noticed you re using the `useBuiltIns` option without declaring a core-js version. Currently, we assume version 2.x when no version is passed. Since this default version will likely change in future versions of Babel, we recommend explicitly setting the core-js version you are using via the `corejs` option.

大致意思是,如果我们使用了 useBuiltIns 选项,建议配置 corejs 选项,如果不配置,默认提供 2.x 版本的 corejs

Warning2

 When setting `useBuiltIns: 'usage'`, polyfills are automatically imported when needed.
 Please remove the `import '@babel/polyfill'` call or use `useBuiltIns: 'entry'` instead.

这个警告是,让我们移除 import '@babel/polyfill ,polyfill 会被自动按需导入加载。所以我们在设置useBuiltIns 为 'usage' 时,不需要手动引入 @babel/polyfill

这里的编译结果,不但对 ES+ 的新增语法进行了转译,而且对类和类的类的扩展属性也进行了转译,结果是比较符合我们期待的,能够直接运行在浏览器上。这里我们看不到 import "@babel/polyfill";  它被拆成小模块,按需引入。

配置四

module.exports = {
  presets: [
    ['@babel/preset-env', {
      useBuiltIns: 'entry',
    }]
  ],
};

实验结果:

"use strict";

require("core-js/modules/es6.array.copy-within");

require("core-js/modules/es6.array.fill");

require("core-js/modules/es6.array.find");

## 中间还有 200+ 个包省略。。。。。。

require("core-js/modules/web.immediate");

require("core-js/modules/web.dom.iterable");

require("regenerator-runtime/runtime");

var cat = 'May';

var arrow = function arrow() {
  return "Name: ".concat(cat).padStart(2);
};

var promise = new Promise();
var map = new Map();

这里同样也得到了同配置四中的 warning1,需要我们配置 corejs 选项,但没有 warning2,同时这里把  import "@babel/polyfill"  拆成小包,全量引入。

我们综合一下配置二三四,分别对 useBuiltIns 的三个可选 option 分别进行了实验,得出了如下结论

  • false:不处理 polyfill
  • 'usage':按需加载 polyfill,且不需要手动引入@babel/polyfill 文件
  • 'entry':必须手动引入 @babel/polyfill 文件,会把 @babel/polyfill 切为小包,全量引入,但要注意的是,这里的全量并不是真的全量,因为我们没有配置目标浏览器,Babbel 默认转了全量的 ECMAScript 2015+,如果配置了如: targets: "chrome>60" ,会在配置四的编译结果中,包减少到 20+ ,也就是 'entry' 会加载目标浏览器所需的 polyfill

配置五

module.exports = {
  presets: [
    ['@babel/preset-env', {
      useBuiltIns: 'usage',
      corejs: 3
    }]
  ],
};

实验结果:

"use strict";

require("core-js/modules/es.array.iterator");

require("core-js/modules/es.map");

require("core-js/modules/es.object.to-string");

require("core-js/modules/es.promise");

require("core-js/modules/es.string.iterator");

require("core-js/modules/es.string.pad-start");

require("core-js/modules/web.dom-collections.iterator");

var cat = 'May';

var arrow = function arrow() {
  return "Name: ".concat(cat).padStart(2);
};

var promise = new Promise();
var map = new Map();

为了解决配置三四中的warning1,我们手动手动设置了 corejs 选项,区别于默认值 2 ,我们设置了 3

配置四的编译结果相比,引用部分发生了变化,默认的 core-js:2 处理依赖是
require("core-js/modules/es6.map");
这里的 core-js:3
require("core-js/modules/es.map");

使用 core-js@3 的原因是,core-js@2 分支中已经不会再添加新特性,新特性都会添加到 core-js@3。例如你使用了 Array.prototype.flat(),如果你使用的是 core-js@2,那么其不包含此新特性。为了可以使用更多的新特性,建议大家使用 core-js@3

到这里好像一切近乎完美,但还有个问题没有处理,抽象和剥离。

配置六

我们改变样本文件:

const promise = new Promise();
class Cat {
  constructor(name){
    this.name= name
  }
  getName(){
    return
  }
}

配置:

module.exports = {
  presets: [
    ['@babel/preset-env', {
      useBuiltIns: 'usage',
      corejs: 3
    }]
  ],
};

实验结果:

"use strict";

require("core-js/modules/es.function.name");

require("core-js/modules/es.object.define-property");

require("core-js/modules/es.object.to-string");

require("core-js/modules/es.promise");

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }

function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }

var promise = new Promise();

var Cat =
/*#__PURE__*/
function () {
  function Cat(name) {
    _classCallCheck(this, Cat);

    this.name = name;
  }

  _createClass(Cat, [{
    key: "getName",
    value: function getName() {
      return;
    }
  }]);

  return Cat;
}();

这里出现了很多公共方法, _classCallCheck  、 _createClass 等,我们统称为 Babel 的注入帮助程序。如果每个文件都这么注入,必然是巨大的浪费资源,这个时候,需要使用 @babel/plugin-transform-runtime

首先安装依赖,@babel/plugin-transform-runtime 通常仅在开发时使用,但是运行时最终代码需要依赖 @babel/runtime,所以 @babel/runtime 必须要作为生产依赖被安装,如下 :

npm install --save-dev @babel/plugin-transform-runtime
npm install --save @babel/runtime

同时,它还会为代码创建一个沙箱环境,这在我们写类库或者工具库时是很有必要的,避免污染全局变量。

配置七

module.exports = {
  presets: [
    ['@babel/preset-env', {
      useBuiltIns: 'usage',
      corejs: 3
    }]
  ],
  plugins: [
    ['@babel/plugin-transform-runtime']
  ],
};

实验结果:

"use strict";

var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");

require("core-js/modules/es.function.name");

require("core-js/modules/es.object.to-string");

require("core-js/modules/es.promise");

var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));

var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));

var promise = new Promise();

var Cat =
/*#__PURE__*/
function () {
  function Cat(name) {
    (0, _classCallCheck2.default)(this, Cat);
    this.name = name;
  }

  (0, _createClass2.default)(Cat, [{
    key: "getName",
    value: function getName() {
      return;
    }
  }]);
  return Cat;
}();

我们发现,之前的帮助函数,是直接定义在文件内部的,如 :

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

现在变成:

var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));

@babel/runtime  统一引入,减少了文件体积。并且对变量名做了处理,避免了全局污染,但同时又发现了新问题,编译后的文件,仅仅对 class 相关的函数做了变量名处理,但是对 Promise 相关的变量名并没有处理。

配置八

module.exports = {
  presets: ['@babel/preset-env'],
  plugins: [
    ['@babel/plugin-transform-runtime', {
      'corejs': 3
    }]
  ]
};

实验结果:

"use strict";

var _interopRequireDefault = require("@babel/runtime-corejs3/helpers/interopRequireDefault");

var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime-corejs3/helpers/classCallCheck"));

var _createClass2 = _interopRequireDefault(require("@babel/runtime-corejs3/helpers/createClass"));

var _promise = _interopRequireDefault(require("@babel/runtime-corejs3/core-js-stable/promise"));

var promise = new _promise.default();

var Cat =
/*#__PURE__*/
function () {
  function Cat(name) {
    (0, _classCallCheck2.default)(this, Cat);
    this.name = name;
  }

  (0, _createClass2.default)(Cat, [{
    key: "getName",
    value: function getName() {
      return;
    }
  }]);
  return Cat;
}();

这样感觉就比较完美了,即实现了对 polyfill 的按需加载,对注入的帮助函数的统一抽象剥离,又实现了对变量的处理,避免污染全局作用域,感觉很香。

四、实验结果和思考

我们通过对 Babel 中基本使用的 @babel/preset-env@babel/plugin-transform-runtime 进行配置,测试了不同配置下的实验结果,得出了比较合适的实践,但出现了一个灵魂的思考,既然 @babel/plugin-transform-runtime  能实现按需加载,沙箱环境,公用函数的统一抽象,还要在 useBuiltIns 里面搞三个参数干啥。这里猜测是考虑到包的体积大小。在 Babel 7.4.0 之后的版本,Babel官方明确建议了不再使用 @babel/polyfill ,建议使用 core-js/stable( polyfill ECMAScript features)和 regenerator-runtime/runtime,(needed to use transpiled generator functions)。

查看原文

赞 1 收藏 1 评论 0

河马嘴不大 发布了文章 · 2019-09-19

再看 Babel

前言

目前鲁班标配为 Babel 7+ 版本

官网地址 https://babeljs.io/docs/en/pl...

开发手册 https://github.com/jamiebuild...

功能

  • 语法转译
  • 转译特性到目标环境(@babel/polyfill)
  • 源码转译

工作机制

原始代码 --> [Babel Plugin] --> 转换后的代码

可以简单的把Babel Preset视为Babel Plugin的集合

presets

官方提供的 presets

  • @babel/preset-env
  • @babel/preset-flow
  • @babel/preset-react
  • @babel/preset-typescript

简写

{
  "presets": [
    "env",
    "@babel/preset-env" // 相等
  ]
}

运行顺序

{
  "presets": [
    "a",
    "b",
    "c"
  ]
}

运行顺序为从后向前,如:c, b ,a

options 机制

{
  "presets": [
    ["@babel/preset-env", {
      "loose": true,
      "modules": false
    }]
  ]
}

plugins

运行顺序

{
  "plugins": [
    "a",
    "b",
    "c"
  ]
}

plugins 在 presets 之前运行,运行顺序为从前向后:a, b ,c

options 机制

{
  "plugins": ["pluginA", ["pluginA"], ["pluginA", {}]]
}
{
  "plugins": [
    [
      "transform-async-to-module-method",
      {
        "module": "bluebird",
        "method": "coroutine"
      }
    ]
  ]
}
查看原文

赞 0 收藏 0 评论 0

河马嘴不大 收藏了文章 · 2019-08-20

exports、module.exports和export、export default到底是咋回事

前言

难得有空,今天开始重新规范的学习一下node编程。
但是引入模块我看到用 require的方式,再联想到咱们的ES6各种exportexport default

阿西吧,头都大了....

头大完了,那我们坐下先理理他们的使用范围。

require: node 和 es6 都支持的引入
export / import : 只有es6 支持的导出引入
module.exports / exports: 只有 node 支持的导出

这一刻起,我觉得是时候要把它们之间的关系都给捋清楚了,不然我得混乱死。话不多少,咱们开干!!

node模块

Node里面的模块系统遵循的是CommonJS规范。
那问题又来了,什么是CommonJS规范呢?
由于js以前比较混乱,各写各的代码,没有一个模块的概念,而这个规范出来其实就是对模块的一个定义。

CommonJS定义的模块分为: 模块标识(module)、模块定义(exports) 、模块引用(require)

先解释 exportsmodule.exports
在一个node执行一个文件时,会给这个文件内生成一个 exportsmodule对象,
module又有一个exports属性。他们之间的关系如下图,都指向一块{}内存区域。

exports = module.exports = {};

clipboard.png

那下面我们来看看代码的吧。

//utils.js
let a = 100;

console.log(module.exports); //能打印出结果为:{}
console.log(exports); //能打印出结果为:{}

exports.a = 200; //这里辛苦劳作帮 module.exports 的内容给改成 {a : 200}

exports = '指向其他内存区'; //这里把exports的指向指走

//test.js

var a = require('/utils');
console.log(a) // 打印为 {a : 200} 

从上面可以看出,其实require导出的内容是module.exports的指向的内存块内容,并不是exports的。
简而言之,区分他们之间的区别就是 exports 只是 module.exports的引用,辅助后者添加内容用的。

用白话讲就是,exports只辅助module.exports操作内存中的数据,辛辛苦苦各种操作数据完,累得要死,结果到最后真正被require出去的内容还是module.exports的,真是好苦逼啊。

其实大家用内存块的概念去理解,就会很清楚了。

然后呢,为了避免糊涂,尽量都用 module.exports 导出,然后用require导入。

ES中的模块导出导入

说实话,在es中的模块,就非常清晰了。不过也有一些细节的东西需要搞清楚。
比如 exportexport default,还有 导入的时候,import a from ..,import {a} from ..,总之也有点乱,那么下面我们就开始把它们捋清楚吧。

export 和 export default

首先我们讲这两个导出,下面我们讲讲它们的区别

  1. export与export default均可用于导出常量、函数、文件、模块等
  2. 在一个文件或模块中,export、import可以有多个,export default仅有一个
  3. 通过export方式导出,在导入时要加{ },export default则不需要
  4. export能直接导出变量表达式,export default不行。

下面咱们看看代码去验证一下

testEs6Export.js

'use strict'
//导出变量
export const a = '100';  

 //导出方法
export const dogSay = function(){ 
    console.log('wang wang');
}

 //导出方法第二种
function catSay(){
   console.log('miao miao'); 
}
export { catSay };

//export default导出
const m = 100;
export default m; 
//export defult const m = 100;// 这里不能写这种格式。

index.js

//index.js
'use strict'
var express = require('express');
var router = express.Router();

import { dogSay, catSay } from './testEs6Export'; //导出了 export 方法 
import m from './testEs6Export';  //导出了 export default 

import * as testModule from './testEs6Export'; //as 集合成对象导出



/* GET home page. */
router.get('/', function(req, res, next) {
  dogSay();
  catSay();
  console.log(m);
  testModule.dogSay();
  console.log(testModule.m); // undefined , 因为  as 导出是 把 零散的 export 聚集在一起作为一个对象,而export default 是导出为 default属性。
  console.log(testModule.default); // 100
  res.send('恭喜你,成功验证');
});

module.exports = router;

从上面可以看出,确实感觉 ES6的模块系统非常灵活的。

代码地址

GitHub: https://github.com/XuXiaoGH/e...

参考文献

1.老树新芽,在ES6下使用Express
2.exports 和 module.exports 的区别
3.module.exports与exports,export与export default之间的关系

感谢这三位前辈的分享。

写在最后

如果文章对你有所帮助,不妨点个赞或者收藏一下,这将是支持我继续写下去的动力。

谢谢亲们。

查看原文

河马嘴不大 发布了文章 · 2019-05-29

前端脚手架构建实践

前面的话

在前端工程化过程中,为了解决多项目中,相似度高的工作,便诞生许多前端脚手架,这里记录下自己实现一个简易前端脚手架过程的实践。主要是解决多个页面相似内容的复制粘贴问题,功能类似于Webstorm的Live template,或者Vscode的Snippets。

思路
  • 预先配置页面模板,预留关键字变量
  • 用户填写关键字变量,生成页面模板,输出到制定目录
用到的包
  • fs

    读写文件模块,这里主要用于读入用户配置文件,输出模板到文件

  • commander

    NodeJs命令行工具,提供了用户命令行输入和参数解析,用户解析用户输入

  • inquirer

    NodeJs交互式命令行工具,询问操作者问题,获取用户输入,校验回答的合法性

  • metalsmith

    文件处理,读写操作

  • handlebars

    将模板中的变量替换为用户输入,编译模板,类似框架如:artTemplate,Jade

  • path

    NodeJs的路径操作库,如合并路径

  • chalk

    命令行输出样式美化

具体实现
  1. 首先在一个新的文件夹,如xxx-tools下 npm init 创建一个node项目,因为是要做成一个npm包的脚手架,所以在包的取名上一定要唯一,即package.jsonname字段,避免在发包的时候和网上已经存在的npm包重名,报403没有权限的错。
  2. 在xxx-tools文件夹下创建bin文件夹,同时在bin文件夹下创建脚本tempTool文件,内容如下:
#!/usr/bin/env node

console.log('Hello World');

注意哦,#!/usr/bin/env node 这个是Linux规范,用来指明了这个执行脚本的解释程序,要是没有这一行,默认用当前Shell去解释这个脚本

  1. package.json中增加bin配置:
 "bin": {
    "tempTool": "./bin/tempTool"
  },
  1. 到目前为止,一个简单的前端脚手架实现了,在npm官网注册,在项目里执行npm login登录,之后npm publish如果一切顺利,npm包提交完毕,可以在其它项目中执行npm i -g xxx-tools,安装这个包,执行xxx-tools命令,输出 Hello World,脚手架开发过程中,也涉及到在本地调试,可以直执行node ./bin/xxx-tools
  2. 现在来加入具体的开发流程,用户的输入,输入信息的读取等等,bin文件修改如下
#!/usr/bin/env node

const program = require('commander');
const chalk = require('chalk');
const { loadTemplate } = require('../src/lib/writeTemp');

const log = data => console.log(chalk.green(data));

log('初始化模板配置');

program
  .command('create')
  .description('create template')
  .option('-d')
  .action(async function () {
    const result = await loadTemplate();
    result ? null : log('配置完毕');
  });

program.parse(process.argv);

用户执行create命令,在这里调用了loadTemplate函数,看一下这个函数

// 把模板中的变量替换为用户输入的变量,输出模板到制定文件夹
const Metalsmith = require('metalsmith');
const Handlebars = require('handlebars');
const path = require('path');
const fs = require('fs');
const { askQuestion } = require('./askQuestion');

const loadTemplate = async () => {
  // 从toolrc.json文件读取配置
  const dirPath = process.cwd();
  if (!fs.existsSync('toolrc.json')) {
    throw new Error('toolrc.json配置文件不存在');
  }
  const configJson = path.join(dirPath, 'toolrc.json');
  const config = fs.readFileSync(configJson);
  const { source, dist, questionConfig } = JSON.parse(config);
  const answer = await askQuestion(questionConfig);
  const metalsmith = Metalsmith(__dirname);

  metalsmith
    .metadata(answer)
    .source(path.join(dirPath, source))
    .destination(path.join(dirPath, dist))
    .use(function (files, metalsmith, done) {
      //遍历替换模板
      Object.keys(files).forEach(fileName => {
        const fileContentsString = files[fileName].contents.toString();
        //Handlebar compile 前需要转换为字符串
        files[fileName].contents = new Buffer(Handlebars.compile(fileContentsString)(metalsmith.metadata()));
      });
      done();
    }).build(function (err) {
    if (err) throw err;
  });
};

module.exports.loadTemplate = loadTemplate;

为了方便用户配置,需要用户自行配置一个toolrc.json文件,指明模板文件的输入输出目录,和需要用到的
询问变量,示例配置如下:

{
  "source": "/src/template",
  "dist": "/build",
  "questionConfig": {
    "name": {
      "type": "string",
      "required": true,
      "label": "Module name"
    },
    "description": {
      "type": "string",
      "required": true,
      "label": "Module description"
    },
    "namespace": {
      "type": "string",
      "required": true,
      "label": "dva model namespace"
    }
  }
}

source配置了模板文件的位置,dist为输出文件的位置,questionConfig为模板中的关键字,需要用户在交互的命令行中输入,下面这段为利用inquirer包,实现命令行交互。

// 遍历问题模板,输出提问

const inquirer = require('inquirer');

const askQuestion =  async (prompts)=> {
  let promptsArr = Object.keys(prompts).map(key => ({
    name: key,
    ...prompts[key]
  }));
  return inquirer.prompt(promptsArr);
}

module.exports.askQuestion = askQuestion

效果如下:
图片描述

因为用了handlebars包,模板的定义需要符合其规范,模板文件如下:

import React, { Component } from 'react';
import { connect } from 'dva';
import './style.less';

@connect(state => ({ loading: state.loading }))
class {{name}} extends React.Component {
  state = {};

  componentWillReceiveProps = (nextProps) => {

  };

  render() {
    return ();
  }

}

export default {{name}};

最终输出到 dist 目录的文件,会替换其中双括号里的内容

结束的话

这里只是简单的例子,可以沉淀一些业务场景的模板,通过命令行的方式快速的创建,避免复制粘贴,其实本意是学习一下Node的脚手架工具的实现,有兴趣的同学可以看看babel-cli的源码。

查看原文

赞 3 收藏 2 评论 1

河马嘴不大 发布了文章 · 2019-01-14

PostCSS原理解析

写在前面的话

前端工程化日益成熟今天,我们对于工具越来越深的封装。不管是从vue-cli3.0版本起,还是umibigfish 等前端脚手架,对于webpack都封装在内,对于工程化无疑是高效的,但也在一定程度上让新手们者失去了从零配置一个项目的机会,所以很多时候还是希望能透过大神们的框架,研究一下底层结构。

简介

PostCSS是一个通过JS插件转换样式表的工具,它本身并不是一门新的CSS语言,而是一个平台或者是生态心态,提供插件扩展服务即JS API,开发者可以根据这些接口,定制开发插件,目前比较流行的插件工具如:Autoprefixer Stylelint CSSnano

工作流

图片描述

大致步骤:

  • 将CSS解析成抽象语法树(AST树)
  • 将AST树”传递”给任意数量的插件处理
  • 将处理完毕的AST树重新转换成字符串

在PostCSS中有几个关键的处理机制:

Source string → Tokenizer → Parser → AST → Processor → Stringifier

Tokenizer

将源css字符串进行分词

举个例子:

.className { color: #FFF; }

通过Tokenizer后结果如下:

[
    ["word", ".className", 1, 1, 1, 10]
    ["space", " "]
    ["{", "{", 1, 12]
    ["space", " "]
    ["word", "color", 1, 14, 1, 18]
    [":", ":", 1, 19]
    ["space", " "]
    ["word", "#FFF" , 1, 21, 1, 23]
    [";", ";", 1, 24]
    ["space", " "]
    ["}", "}", 1, 26]
]

word类型为例,参数如下:

const token = [
     // token 的类型,如word、space、comment
    'word',

    // 匹配到的词名称
    '.className',

    // 代表该词开始位置的row以及column,但像 type为`space`的属性没有该值
    1, 1,

    // 代表该词结束位置的row以及column,
    1, 10
]

Parser

经过Tokenizer之后,需要Parser将结果初始化为AST

this.root = {
    type: 'root',
    source: { input: {css: ".className { color: #FFF; }", hasBOM: false, id: "<input css 1>"},
                   start: { line: 1, column: 1 } ,
                  end: { line: 1, column: 27 }
    },
   raws:{after: "", semicolon: false}
   nodes // 子元素
}

Processor

经过AST之后,PostCSS提供了大量JS API给插件用

Stringifier

插件处理后,比如加浏览器前缀,会被重新Stringifier.stringify为一般CSS。

结语

PostCSS更多的是提供平台能力,赋能js的处理。

查看原文

赞 2 收藏 2 评论 0

河马嘴不大 发布了文章 · 2019-01-07

前端代码风格自动化系列(五)之共同出击

我们在前面的四篇中介绍了huskycommitlintlint-stagedprettier这些工具,可以完成以最小的代价在Git提交到远程仓库前,格式化为统一风格的代码,eslint大家都很熟悉这里就不列举了。下面举一个配置。

{
  "devDependencies": {
    "babel-eslint": "^10.0.1",
    "eslint": "^5.4.0",
    "eslint-config-airbnb": "^17.0.0",
    "eslint-config-prettier": "^3.0.1",
    "eslint-plugin-babel": "^5.1.0",
    "eslint-plugin-compat": "^2.6.2",
    "eslint-plugin-import": "^2.14.0",
    "eslint-plugin-jsx-a11y": "^6.1.2",
    "eslint-plugin-markdown": "^1.0.0-beta.6",
    "eslint-plugin-react": "^7.11.1",
    "husky": "^1.1.2",
    "lint-staged": "^8.0.4",
    "prettier": "1.14.3",
    "stylelint": "^9.4.0",
    "stylelint-config-prettier": "^4.0.0",
    "stylelint-config-standard": "^18.0.0",
    "tslint": "^5.10.0",
    "tslint-config-prettier": "^1.10.0",
    "tslint-react": "^3.6.0",
  },
  "lint-staged": {
    "**/*.{js,jsx,less}": [
      "prettier --write",
      "git add"
    ],
    "**/*.{js,jsx}": "npm run lint-staged:js",
    "**/*.less": "stylelint --syntax less"
  },
  "husky": {
    "hooks": {
      "pre-commit": "npm run lint-staged",
      "commit-msg": "commitlint -e $HUSKY_GIT_PARAMS"
    }
  }
}

这里对于项目里tsjsjsxless分别做了提交前格式化操作,对于提交规范做了校验。

查看原文

赞 1 收藏 1 评论 0

河马嘴不大 发布了文章 · 2019-01-07

前端代码风格自动化系列(四)之Prettier

Prettier是一个支持多语言的代码格式工具,如常用的:jsjsxVueFlowTsHTMLCSS等,非常全面,将代码解析为AST,然后重新组装,目的是最终输出风格统一的代码,对比eslint对error的fix要强一些,如最大长度的改动,eslint只是对有问题的地方进行格式化修改,不改动源代码风格,而prettier是对全量的代码进行格式化。

安装

npm install --save-dev prettier

配置

// package.json
{
  "husky": {
    "hooks": {
      "pre-commit": "lint-staged"
    }
  },
  "lint-staged": {
    "*.{js,json,css,md}": ["prettier --write", "git add"]
  }
}

这里我们结合之前用到的huskylint-staged,默认prettier是直接标准输出到终端的,--write,这个配置代表直接改写文件。

这里有个官网的例子

foo(reallyLongArg(), omgSoManyParameters(), IShouldRefactorThis(), isThereSeriouslyAnotherOne());

格式化之后

foo(
  reallyLongArg(),
  omgSoManyParameters(),
  IShouldRefactorThis(),
  isThereSeriouslyAnotherOne()
);

prettier让我们专注于业务逻辑,无需再纠结代码风格,配合其它工具,实现了代码提交到仓库前,统一格式化。

查看原文

赞 1 收藏 1 评论 0

河马嘴不大 发布了文章 · 2019-01-07

前端代码风格自动化系列(三)之Lint-staged

在我们介绍了Husky、Commitlint之后,来看一个前端文件过滤的工具Lint-staged,代码的格式化肯定会涉及到文件系统,一般工具会首先读取文件,格式化操作之后,重新写入。对于较大型的项目,文件众多,首先遇到的就是性能问题,虽然如Eslint之类的也有文件过滤配置,但毕竟还是对于匹配文件的全量遍历,如全量的.js文件,基本达不到性能要求,有时还会误格式化其他同学的代码,因此我们引入Lint-staged,一个仅仅过滤出Git代码暂存区文件(被committed的文件)的工具。

安装

npm install --save-dev lint-staged husky

配置

首先明确一下,Lint-staged仅仅是文件过滤器,不会帮你格式化任何东西,所以没有代码规则配置文件,需要自己配置一下,如:.eslintrc.stylelintrc等,然后在package.json中引入。

{
  "husky": {
    "hooks": {
      "pre-commit": "lint-staged"
    }
  },
  "lint-staged": {
    "*.js": ["eslint --fix", "git add"]
  }
}

当文件变化,我们git commit它们,pre-commit钩子会启动,执行lint-staged命令,我们对于lint-staged如上文配置,对本次被commited中的所有.js文件,执行eslint --fix命令和git add,命令,前者的的目的是格式化,后者是对格式化之后的代码重新提交。

除了在package.json中配置,也可以在.lintstagedrclint-staged.config.js文件中,lint-staged的常用选项除了liners之外,还有ignoreconcurrent 等,具体参考文档:

{
  "lint-staged": {
    "linters": {
      "*.{js,scss}": ["some command", "git add"]
    },
    "ignore": ["**/dist/*.min.js"]
  }
}

对于文件的过滤,lint-staged的格式如下:

{
  // .js files anywhere in the project
  "*.js": "eslint",
  // .js files anywhere in the project
  "**/*.js": "eslint",
  // .js file in the src directory
  "src/*.js": "eslint",
  // .js file anywhere within and below the src directory
  "src/**/*.js": "eslint",
}

lint-staged提供的功能远不止于此,它只是平台,具体的格式化工具的搭配有很多,如对于图片的、样式的、.tsx.md等文件的。

查看原文

赞 9 收藏 5 评论 0

河马嘴不大 发布了文章 · 2019-01-07

前端代码风格自动化系列(二)之Commitlint

在有了Husky赋能之后,我们有能力在Git的钩子里做一些事情,首先不得不提的是代码的提交规范和规范的校验,优雅的提交,方便团队协作和快速定位问题。首推Commitlint,另外@加神 推荐了Gitmoji也是一个很有意思的工具。

安装

npm install --save-dev @commitlint/config-conventional @commitlint/cli

// 生成配置文件commitlint.config.js,当然也可以是 .commitlintrc.js
echo "module.exports = {extends: ['@commitlint/config-conventional']};" > commitlint.config.js

配置

在husky的配置加入CommitlIint配置,v1.0.1版本以后为HUSKY_GIT_PARAMSv0.14.3GIT_PARAMS

"husky": {
    "hooks": {
      "pre-commit": "npm run test",
      "commit-msg": "commitlint -e $HUSKY_GIT_PARAMS"
    }
  },

定制提交规范

提交格式(注意冒号后面有空格)

<type>: <subject>

常用的type类别

  • upd:更新某功能(不是 feat, 不是 fix)
  • feat:新功能(feature)
  • fix:修补bug
  • docs:文档(documentation)
  • style: 格式(不影响代码运行的变动)
  • refactor:重构(即不是新增功能,也不是修改bug的代码变动)
  • test:增加测试
  • chore:构建过程或辅助工具的变动

例子:

git commit -m 'feat: 增加 xxx 功能'
git commit -m 'bug: 修复 xxx 功能'

subject

subject是 commit 目的的简短描述,可以做一些配置,如最大长度限制。

commitlint.config.js文件配置

rule配置说明::rule由name和配置数组组成,如:'name:[0, 'always', 72]',数组中第一位为level,可选0,1,2,0为disable,1为warning,2为error,第二位为应用与否,可选always|never,第三位该rule的值。具体配置例子如下:

module.exports = {
  extends: [
    "@commitlint/config-conventional"
  ],
  rules: {
    'type-enum': [2, 'always', [
      'upd', 'feat', 'fix', 'refactor', 'docs', 'chore', 'style', 'revert'
     ]],
    'type-case': [0],
    'type-empty': [0],
    'scope-empty': [0],
    'scope-case': [0],
    'subject-full-stop': [0, 'never'],
    'subject-case': [0, 'never'],
    'header-max-length': [0, 'always', 72]
  }
};

这里列出了大部分常用的配置,其它的可以参考Commitlint网站,具体使用例子:

图片描述

这里我们使用错误的提交方式,最上面的是自动测试的脚本,大家可以忽略,husky给出了commit-msg的input为xxx,触发了subject-emptytype-empty两个规则,提交不符合规范,被拦了下来。如果是正确的提交,例子如下:

图片描述

关于Commitlint的使用就到这里了。

查看原文

赞 18 收藏 7 评论 0

认证与成就

  • 获得 71 次点赞
  • 获得 1 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 1 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2018-05-18
个人主页被 1.6k 人浏览