Coder Social home page Coder Social logo

blog's Introduction

Hi there 👋

blog's People

Contributors

eamonnzhang avatar

Stargazers

 avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

Forkers

feimao90

blog's Issues

用vue-cli3从0打造一个完整的UI库

前言

本文旨在给大家提供一种构建一个完整UI库脚手架的思路:包括如何快速并优雅地构建UI库的主页、如何托管主页、如何编写脚本提升自己的开发效率、如何生成CHANGELOG等等,这里提供了一个Demo可供大家参考:

在线Demo地址

GitHub源码地址

初始化目录结构

主流的开源UI库代码结构主要分为三大部分:

  1. 组件库本身的代码:这部分代码会发布到npm上
  2. 预览示例和查看文档的网站代码:类似VantElementUI这类网站。
  3. 配置文件和脚本文件:用于打包和发布等等

我们先用vue-cli3初始化一个模板,然后在根目录下新增三个文件夹,一个用来存放 组件 的代码(packages),一个用来存放 预览示例的网站 代码(examples)(这里直接把初始化模板的src目录更改为examples即可),一个用来存放 脚本 代码(build

文件的名字可以根据自己喜好去命名。

完成一个组件

具体的代码组织方式可以查看Github上的源码,这里需要注意,我们尽量以组件名来命名文件夹名,然后在文件夹内新建index.vue组件。这么做是为了方便后面我们用代码直接生成index.js入口文件的内容。

样式文件的分离

为了更好的处理样式,我们把所有的样式文件单独处理(代码地址),这里主要用Gulp来处理任务。

发布NPM包

打包

当我们完成了一个组件,就可以打包了,打包很简单,这得益于vue-cli3的build命令引入了构建目标参数,只需执行

vue-cli-service build --target lib --name vue-cards --dest lib packages/index.js

就可以把packages下所有的代码以模式打包出去。

在库模式中,Vue 是外置的。这意味着包中不会有 Vue,即便你在代码中导入了 Vue。如果这个库会通过一个打包器使用,它将尝试通过打包器以依赖的方式加载 Vue;否则就会回退到一个全局的 Vue 变量。

发布

发布之前,我们需要新增一个.npmignore文件,把一些不需要发布到npm包的文件或者文件夹都写在里面,写法和.gitignore一致。
具体怎么发布一个npm包这里就不在赘述了。

文档的编写和发布

编写

可能大多数人觉得写一个开源UI库最头疼的事情就是写文档,如何快速又优雅的构建出像ElementUI官网这类网站呢?
目前最流行的写文档的方式就是通过markdown编写,那我们要解决的就是如何将Markdown文档HTML化,直接展示在页面中。
这里我们可以用vue-markdown-loader插件,该插件的具体配置可以查看项目的vue.config.js文件。
具体到本项目的效果如图所示:
2df0a32b677d190ffda5ce70226e160e.png
代码示例部分都是用markdown编写的,其他部分则是普通的vue组件。

本项目网站的具体的代码地址

发布

我们可以利用GithubPages功能来发布我们的文档网站,具体的使用方法网上有很多教程,比如GitHub Pages 使用入门
其实还有一种很简单的方式,可以用gh-pages这个工具,一键发布到对应仓库的gh-pages分支。具体配置和使用可以参考本项目的build/publish-docs.js文件。

提高开发效率的一些做法

在本项目的build文件夹下,有很多能够大大提升我们开发效率的文件,下面来一一讲解一下它们的用途。

get-components.js文件主要为了获取packages目录下所有的组件目录,为我们构建packages下的入口文件做准备。

build-entry.js文件主要是根据get-components.js的结果,然后将代码写入/packages/index.js,生成入口文件。

build-lib.js文件主要是做一些发布npm包前的构建准备,包括构建入口文件、构建库、构建样式文件等。

publish-docs.js文件的作用是可以一键发布文档到本仓库的gh-pages分支。

release.sh脚本文件主要是把一些发布npm包的命令集合在了一起,包括很多git和npm操作。

快速生成CHANGELOG

CHANGELOG我们可以自己手写MD文档,当然这并不明智。我们更希望的是用代码去自动化生成,原理无非就是提取你的git提交信息并写入文件,这里我们可以用conventional-changelog来生成CHANGELOG。为了生成更精细和正确的CHANGELOG文档,我们需要严格规范自己的提交记录,我们可以用conventional-changelog的标准提交记录:cz-conventional-changelog,具体的用法可以参考cz-cli。本项目的package.jsoninit脚本其实也封装了相关的脚本命令,可以参考。

参考

  1. vant - Lightweight Mobile UI Components built on Vue
  2. element - A Vue.js 2.0 UI Toolkit for Web
  3. vue - Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
  4. 详解:Vue cli3 库模式搭建组件库并发布到 npm

Babel初探-构成

Babel名称的由来

Babel的前身的叫6to5,顾名思义它最初的作用就是把ES6转化为ES5,但是Babel的创始人一直觉得这个名字并没有传达出团队的真正目标,他们想做的不仅仅是把ES6转化为ES5这么单一的事情,况且现在ES6又被叫作ES2015,所以这个名字总归会引起很多歧义,于是团队决定把名字重新命名为 Babel

Babel 一词来源于道格拉斯·亚当斯(Douglas Adams)所著的《银河系漫游指南》里的巴别鱼(BabelFish),它是一个能帮助人类理解任何语言的虚构物种,同时也恰好向巴别塔(Babel Tower)的故事致敬,《创世纪》中正是巴别塔的坠落造就了各种各样的语言。

Babel的构成

截止到目前(2018/11/08),Babel已经发布到了第七个版本,7和6的包除了命名方式上有变化(7全部以@babel/*而非babel-*)之外,构成方式的变化并不算太大(之后会更新Babel6和7的异同)。值得注意的是,babel从5升级到6对包的构成进行了一个重大的改版,很多包被分离了出来,更注重了模块化和插件化,而不再是像5一样的“一锅大杂烩”。
从Github可以看到babel5.x版本的packages构成,只有几个包,但是切换到babel6.x以上的版本可以看到多了很多包,不要被这么多包吓到,我们来仔细梳理一下,根据前缀大致把包可以分为以下四大类:

  1. babel-*
  2. babel-plugin-*
  3. babel-preset-*
  4. babel-helper-*

babel采用monorepo来管理自己module

接下来将以Babel7.x为基础讲解。

核心包和外围包(babel-*)

第一类的包大致可以分为两种:核心包外围包
核心包(Core Packages):

  • @babel/core
    这个包就是Babel编译器本身,它暴露了babel.transform方法,用于转换源代码。babel编译器又可以分解下面的三个部分。
  • @babel/parser
    这个包是Babel的语法分析器,用于将源代码转化为AST。
  • @babel/traverse
    这个包是Babel的遍历器,用于遍历AST。
  • @babel/generator
    这个包是Babel的生成器,用于把AST转换为最终代码。

其实从这个四个核心包就能基本看出Babel的工作流程和原理大致就是:

输入字符串 -> @babel/parser 编译 -> AST -> transformer[s] 使用 @babel/traverse 去遍历上一步AST并进行一些操作 -> AST -> @babel/generator 把最终的AST再次生成代码 -> 输出字符串

外围包(Other Packages):

  • @babel/cli
    是运行@babel/core的命令行工具,同时也包含了@babel/node运行环境。
  • @babel/types
    可以理解为是一个工具包,被用来验证、构建和改变AST的节点。
  • @babel/polyfill
    可以理解为把core-jsregenerator-runtime包在一起的包装器。
  • @babel/runtime
    这个包和polyfill很相似,唯一的区别在于它不会更改全局的作用域,经常和@babel/plugin-transform-runtime一起使用(后面会讲为什么)。
  • @babel/register
    这个包通过绑定到node的require中,实现程序在运行的时候自动编译文件。
  • @babel/template
    是一个帮助函数,它允许从代码的字符串表示中构造AST节点;这消除了使用@babel/types构建AST节点的繁琐。
  • @babel/helpers
    在一些babel插件中使用的一组预先制作的@babel/template功能。
  • @babel/code-frame
    是一个独立的包,主要用于打印错误和异常,会打印出源代码和错误指向的位置。

ok,Babel一些很重要的包都介绍完了,那是不是装了@babel/core之后,然后调用transform方法就能直接编译出我们想要的代码了呢?比如:

const babel =  require('@babel/core')

let transformCode  = babel.transform('(x)=>x')

console.log(transformCode.code)

结果:
image

发现并没有任何作用,所以如果想让编译有效果,还需要下面两类包的配置,其实这也是Babel升级为6版本之后插件化的精髓所在。

安装Babel不就是为了使用es的所有新特性么,为什么还要弄个插件配置来说明我要用哪些新特性? - 尤雨溪的回答 - 知乎
https://www.zhihu.com/question/61124018/answer/184718397

插件包(babel-plugin-*)

插件包是Babel的核心,也是让Babel能够产出我们想要的代码的一类包。
插件包大致又可分为三类:

  • babel-plugin-transform-*
    顾名思义这类插件的作用在于转译,Babel官方维护了很多转译插件,比如将ES6/ES2015转换为ES5,转换为ES3、minification、JSX、flow还有一些正在实验的特性等等的插件。这里有一个transform插件列表
  • babel-plugin-syntax-*
    这些插件仅仅是为了让转译插件能够解析某些特性,实际上转译插件已经包含了语法插件,所以无需二者都配置。
  • babel-plugin-proposal-*
    之前在Babel6中,所有包含 stage-x 的转译插件都使用了这个前缀,但是不包含语法插件(syntax plugins)。

由此可见Babel将插件拆分的特别细,但是一个前端项目往往是庞大的,使用的各种语法也很繁杂,并且运行浏览器环境是不确定的,比如有的项目需要兼容IE9,有的只在现代浏览器里运行,这都会对插件配置的有影响,如果一个一个插件配置显然是很繁琐的,因此Babel又帮我们预置了一些插件集合,就是下面一类包。

预置包(babel-preset-*)

预置包其实就是插件的一个集合,这里“隆重”推荐@babel/preset-env
env代表环境的意思,也就是说这个包可以根据当前你想要配置的执行的环境的版本来引入不同的插件,Babel会自动计算不同环境版本之间对不同新特性之间支持的差异,从而做到最优。

辅助包(babel-helper-*)

这类包主要用于各种其他插件的内部使用。

参考链接
一口(很长的)气了解 babel
Babel6的学习新姿势
Babel用户手册

0.1+0.2 !== 0.3?

前言

众所周知,JavaScript在计算某些浮点数的运算时会出现精度的丢失,比如你在控制台输入0.1+0.2,得到的结果是0.30000000000000004而不是0.3,原因是什么?

世界上有两种人,懂二进制和不懂二进制的人

我们知道,计算机里所有的数据最终都是以二进制保存的,当然数字也一样。所以当计算机计算0.1+0.2的时候,实际上计算的是这两个数字在计算机里所存储的二进制,那么0.1在JavaScript里存储的二进制到底是多少?
我们先根据十进制转二进制的方法,把0.1转化为二进制是:0.0001100110011001100...(1100循环),然后把0.2转化为二进制是:0.00110011001100...(1100循环)。
我们发现,它们都是无限循环的二进制。显然,计算机当然不会用自己“无限的空间”去存储这些无限循环的二进制数字。那对于这类数据该怎么办?

JavaScript如何存储无限循环的二进制小数?

不同的语言可能会有不同的存储标准,JavaScript中所用的数字包括整数和小数,都只有一种类型就是Number,它的实现遵循IEEE 754标准,使用64位固定长度来表示,也就是标准的double双精度浮点数(相关的还有float 32位单精度),具体的双精度浮点数的存储方式这里不再赘述(可以看后面章节的详细描述),我们只需要知道,在二进制科学表示法中,双精度浮点的小数部分最多只能保留52位(比如1.xxx...*2^n,这里x最多保留52位)加上前面的1,其实就是保留53位有效数字,剩余的舍去,遵从“0舍1入”,那么0.1的二进制舍去之后就是:

0.00011001100110011001100110011001100110011001100110011010

同理我们得到0.2的舍去之后的二进制表示为:

0.0011001100110011001100110011001100110011001100110011010

二者相加得到:

0.00011001100110011001100110011001100110011001100110011010 +
0.0011001100110011001100110011001100110011001100110011010 =
0.0100110011001100110011001100110011001100110011001100111

我们把结果根据公式或者工具转为十进制:

可以看到结果正好为:0.30000000000000004

注:大多数语言中的小数默认都是遵循 IEEE 754 的 float 浮点数,包括 Java、Ruby、Python,本文中的浮点数问题同样存在。

浮点数是如何保存的


在计算机中,浮点表示法,分为三大部分,如上图所示:

  • 第一部分(蓝色)用来存储符号位(sign),用来区分正负数,0表示正数
  • 第二部分(绿色)用来存储指数(exponent)
  • 第三部分(红色)用来存储小数(fraction)

双精度浮点数一共占据64位:

  • 符号位(sign)占用1位
  • 指数位(exponent)占用11位
  • 小数位(fraction)占用52位

这里的符号位、指数位、小数位是和科学记数法联系在一起的。
我们以78.735为例

最后的1.001110011*2^6就是科学记数法,这个实数由一个整数或定点数(即尾数)乘以某个基数(计算机中通常是2)的整数次幂得到,这就叫浮点数
那我们不妨根据这个规定,对号入座,把78.735转化为双精度的表示法,符号位和小数位很明显能看出来,只需要把指数部分6转化为二进制是110就可以了,最终为:

0(sign) 00000000110(exponent) 00111001 10000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000

(这个结果其实错误的,具体为什么错,继续看下文)

我们再根据双精度规范,来看看上文提到的0.1到底是如何存储的,我们已知它的二进制是:

0.00011001100110011001100110011001100110011001100110011001 10011...

转化为科学表示法就是:

1.1001100110011001100110011001100110011001100110011001*2^-2

也就是说0.1的:

  • 符号位为:0
  • 小数位为:1001100110011001100110011001100110011001100110011001
  • 指数位为:-2

到这里我就懵逼了,-2怎么转为二进制呢,虽然双精度浮点规范规定了一个符号位,但是这个符号位表示的是整个数据的正负,而非指数的正负,难道还要保留一位专门存储指数的正负吗?答案是否定的。

指数位为负数的怎么保存?

为了减少不必要的麻烦,IEEE规定了一个偏移量,这个偏移量是干嘛用的呢,就是对于指数部分,每次都加这个偏移量进行保存,这样即使指数是负数,那么加上这个偏移量也变为正数啦。为了使所有的负指数加上这个偏移量都能够变为正数,这个偏移量的设置也是有规律的。
以double双精度为例,我们知道它的指数部分是二进制的11位,那么能够表示的数据范围就是0~2047,IEEE规定1023为双精度的偏移量。

  1. 当指数位不全是0也不全是1时(规格化的数值),IEEE规定,阶码计算公式为 e-Bias。 此时e最小值是1,则1-1023= -1022,e最大值是2046,则2046-1023=1023,可以看到,这种情况下取值范围是-1022~1013
  2. 当指数位全部是0的时候(非规格化的数值),IEEE规定,阶码的计算公式为1-Bias,即1-1023= -1022
  3. 当指数位全部是1的时候(特殊值),IEEE规定这个浮点数可用来表示3个特殊值,分别是正无穷,负无穷,NaN(not a number)。 具体的,小数位不为0的时候表示NaN;小数位为0时,当符号位s=0时表示正无穷,s=1时候表示负无穷。

这个时候我们再看78.735的到底该如何转化,指数部分存储的时候需要加上偏移量6+1023就是1029,转化为二进制就是:
10000000101,所以78.735正确存储方式为:

0 10000000101 00111001 10000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000

同理,你是否也知道0.1的双精度的浮点存储形式了呢?

浮点数值的范围

如果你认真读到了这里,想必你应该能推算出JavaScript的所能表示的数值范围了吧。
e的最大值是1023。
1.111..(52位)..11*2^1023
转为普通二进制就是:

1 111..(52位)..11 000..(971位)..00

把二进制转为十进制就是:

我们会发现这个值和Number.MAX_VALUE的值一致,都是1.7976931348623157e+308
但实际上这个值还不算最大,比如我们在此数值基础上继续加一些数,发现并没有返回Infinity

所以Number.MAX_VALUEInfinity之间还存在很多数,根据IEEE规范我们可以得知,正无穷当且仅当是指数部分全为1(指数部分的最大值Math.pow(2,11)-1-1023 == 1024),小数部分为0的时候,就是:

1.000...*2^1024

所以Math.pow(2,1024)就是正无穷,那么其实JavaScript所能存储的最大数字是Math.pow(2,1024)-1
但是Number.MAX_VALUEMath.pow(2,1024)之间的数据我们无法正常表示出来,精度会丢失。
同理也可推算最小数。

JavaScript的最大安全整数

所谓安全范围,就是我们在这个范围内计算不会出现精度的丢失。
根据双精度的定义,可以得知,最大的安全整数:

1.11..(52位)*2^52

转为十进制就是Math.pow(2,53)-1,即9007199254740991

在JavaScript中,有Number.MAX_SAFE_INTEGER来表示最大安全整数

我们发现和我们自己推算出来的值是一样的。

如何解决计算误差问题

这里推荐Number-Precision库,不到1K的体积。

参考文章:

  1. 抓住数据的尾巴
  2. java浮点类型float和double的主要区别,它们的小数精度范围大小是多少? - Boss呱呱的回答 - 知乎

Babel初探-使用和配置

如何使用Babel?

讲如何配置Babel之前,我们先来看看怎么使用Babel。
从JS运行环境上分类,可分为两种:

浏览器环境

浏览器环境下如何使用Babel取决于你如何处理和编写JS代码。

没有使用任何打包或者构建工具(直接在HTML文档中插入ES6脚本)
<div id="output"></div>
<!-- 加载 Babel -->
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
<!-- 你的脚本代码 -->
<script type="text/babel">
const getMessage = () => "Hello World";
document.getElementById('output').innerHTML = getMessage();
</script>

如代码所示,只需在头部引入babel-standalone@6/babel.min.js",然后把ES6代码写在<script>标签中,注意标签的type="text/babel"
这种使用Babel的方式非常不推荐,首先是因为引入了babel-standalone@6/babel.min.js",这个模块里包含了所有Babel的插件和功能,全特性支持,但是体积接近1MB,造成了多余的请求;其次是因为实时的转码也会浪费时间和资源,性能也不高。所以这种方式只推荐在开发和学习中使用,生产环境万万不可。

使用Webpack等打包工具

针对Webpack的打包,只需引入babel-loader即可转译JS代码:

module: {
  rules: [
    { test: /\.js$/, exclude: /node_modules/, loader: "babel-loader" }
  ]
}

其他的打包工具也是根据工具配置文档来配置,方式大同小异,不再赘述。

使用Gulp等构建工具

Gulp为例,只需引入gulp-babel

var gulp = require("gulp");
var babel = require("gulp-babel");

gulp.task("default", function () {
  return gulp.src("src/app.js")
    .pipe(babel())
    .pipe(gulp.dest("dist"));
});

其他构建工具使用方法也类似,不再赘述。

Node环境

Babel提供了babel-register模块,只需:

require("babel-register");
require("other ES6&7&8 Module");

这个包重写了require命令,加上了一个钩子。此后通过 node 引入的带 .es6, .es, .jsx.js 后缀的所有后续文件都将会被 Babel 转译。
这种方式也是实时转码,所以也只适合在开发环境中使用。

友情提示,Node发展到今日(2018/11/10)已经基本上支持了所有(99%~100%)的包括ES2015、ES2016、ES2017、ES2018的特性,详见Node.js ES Support,所以要想使用新的ES特性,请尽快升级你的Node版本吧!

如何配置Babel?

Babel有两种并行的文件配置,可以一起用,也可以单独使用,都是放在项目或者模块的根目录下:

项目范围内的配置(Project-wide configuration)

babel.config.js,写法如下:

module.exports = function () {
  const presets = [ ... ];
  const plugins = [ ... ];

  return {
    presets,
    plugins
  };
}

文件相关的配置(File-relative configuration)

.babelrc或者package.jsonbabel字段,写法如下:
.babelrc

{
  "presets": [...],
  "plugins": [...]
}

package.json

{
  "name": "my-package",
  "version": "1.0.0",
  "babel": {
    "presets": [ ... ],
    "plugins": [ ... ],
  }
}

那么读到这里你应该奇怪,为什么要用两种文件配置,直接用一种不好吗?下面我来粗浅的讲一下,二者的异同。

两种配置的异同

这两种配置方式差异主要体现在Babel向上级目录查找的方式。
.babelrc的查找机制是这样的:
Babel从被编译的文件开始向上查找该文件,合并以及重写配置,一旦找到包含package.json的目录,搜索将停止,因此.babelrc只应用于单个模块中。
这会带来一个问题就是,比如我们用了第三方的node_modules或者其他模块,我们想在自己的项目里重写它们的Babel配置变的十分困难,直接去修改该模块的.babelrc显然是不友好的。
所以Babel7.x引入了babel.config.js,和.babelrc查找机制不同的地方在于,Babel对于这个文件会一直向上查找,查到一个根目录(后面对这个目录会做讲解),然后读取该目录下的babel.config.js,最后合并以及重写配置。这个根目录,可以理解为是当前工作的目录,但是如果babel.config.js被放在了如下目录,将会被直接忽略:

babel.config.js
packages/
  mod1/
    package.json
    src/index.js
  mod2/
    package.json
    src/index.js

必须放在packages目录下才有效。当然也可以通过设置"configFile"来指定配置文件的位置,最终也可以被Babel读取到。

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.