引言

因为业务须要,近期团队要搞一套本人的UI组件库,框架方面还是Vue。而业界曾经有比拟成熟的一些UI库了,比方ElementUIAntDesignVant等。

联合框架Vue,咱们抉择在ElementUI根底上进行革新。但造轮子绝非易事,首先须要先去理解它整个但构建流程、目录设计等。

本文通过剖析ElementUI残缺的构建流程,最初给出搭建一个齐备的组件库须要做的一些工作,心愿对于想理解ElementUI源码或者也有搭建UI组件库需要的你,能够提供一些帮忙!

咱们先来看下ElementUI的源码的目录构造。

目录构造解析

  • github:寄存了Element UI奉献指南、issuePR模板
  • build:寄存了打包相干的配置文件
  • examples:组件相干示例 demo
  • packages:组件源码
  • src:寄存入口文件和一些工具辅助函数
  • test:单元测试相干文件,这也是一个优良的开源我的项目必备的
  • types:类型申明文件

说完文件目录,剩下还有几个文件(常见的.babelrc.eslintc这里就不开展阐明了),在业务代码中是不常见的:

  • .travis.yml:继续集成(CI)的配置文件
  • CHANGELOG:更新日志,这里Element UI提供了四种不同语言的,也是很贴心了
  • components.json:表明了组件的文件门路,不便 webpack 打包时获取组件的文件门路。
  • FAQ.md:ElementUI 开发者对常见问题的解答。
  • LICENSE:开源许可证,Element UI应用的是MIT协定
  • Makefile:Makefile 是一个实用于 C/C++ 的工具,在领有 make 环境的目录下, 如果存在一个 Makefile 文件。 那么输出 make 命令将会执行 Makefile 文件中的某个指标命令。

深刻理解构建流程前,咱们先来看下ElementUI 源码的几个比拟次要的文件目录,这对于前面钻研ElementUI的残缺流程是有帮忙的。

package.json

通常咱们去看一个大型项目都是从package.json文件开始看起的,这外面蕴含了我的项目的版本、入口、脚本、依赖等要害信息。

我这里拿出了几个关键字段,一一的去剖析、解释他的含意。

main

我的项目的入口文件

import Element from 'element-ui' 时候引入的就是main中的文件

lib/element-ui.common.jscommonjs标准,而lib/index.jsumd标准,这个我在前面的打包模块会具体阐明。

files

指定npm publish发包时须要蕴含的文件/目录。

typings

TypeScript入口文件。

home

我的项目的线上地址

unpkg

当你把一个包公布到npm上时,它同时应该也能够在unpkg上获取到。也就是说,你的代码既可能在NodeJs环境也可能在浏览器环境执行。为此你须要用umd格局打包,lib/index.jsumd标准,由webpack.conf.js生成。

style

申明款式入口文件,这里是lib/theme-chalk/index.css,前面也会具体阐明。

scripts

开发、测试、生产构建,打包、部署,测试用例等相干脚本。scripts算是package.json中最重要的局部了,上面我会一一对其中的重要指令进行阐明。

bootstrap

"bootstrap": "yarn || npm i"

装置依赖, 官网举荐优先选用yarn(吐槽一句:我刚开始没看明确,想着bootstrap不是之前用过的那个 ui 库吗 ????,起初看了下,原来bootstrap翻译过去是疏导程序的意思,这样看看也就大略了解了 ????)

build:file

该指令次要用来自动化生成一些文件。

"build:file": "node build/bin/iconInit.js & node build/bin/build-entry.js & node build/bin/i18n.js & node build/bin/version.js"

这条指令较长,咱们拆开来看:

build/bin/iconInit.js

解析icon.scss,把所有的icon的名字放在icon.json外面 最初挂在Vue原型上的$icon上。

最初通过遍历icon.json,失去了官网的这种成果:

build/bin/build-entry.js

依据components.json文件,生成src/index.js文件,外围就是json-templater/string插件的应用。

咱们先来看下src/index.js文件,他对应的是我的项目的入口文件,最下面有这样一句:

/* Automatically generated by './build/bin/build-entry.js' */

也就是src/index.js文件是由build/bin/build-entry.js脚本主动构建的。咱们来看下源码:

// 依据components.json生成src/index.js文件// 引入所有组件的依赖关系var Components = require("../../components.json");var fs = require("fs");// https://www.npmjs.com/package/json-templater 能够让string与变量联合 输入一些内容var render = require("json-templater/string");// https://github.com/SamVerschueren/uppercamelcase  转化为驼峰 foo-bar >> FooBarvar uppercamelcase = require("uppercamelcase");var path = require("path");// os.EOL属性是一个常量,返回以后操作系统的换行符(Windows零碎是\r\n,其余零碎是\n)var endOfLine = require("os").EOL;// 生成文件的名字和门路var OUTPUT_PATH = path.join(__dirname, "../../src/index.js");var IMPORT_TEMPLATE =  "import {{name}} from '../packages/{{package}}/index.js';";var INSTALL_COMPONENT_TEMPLATE = "  {{name}}";// var MAIN_TEMPLATE = `/* Automatically generated by './build/bin/build-entry.js' */// ...// 获取所有组件的名字,寄存在数组中var ComponentNames = Object.keys(Components);var includeComponentTemplate = [];var installTemplate = [];var listTemplate = [];ComponentNames.forEach((name) => {  var componentName = uppercamelcase(name);  includeComponentTemplate.push(    render(IMPORT_TEMPLATE, {      name: componentName,      package: name,    })  );  if (    [      "Loading",      "MessageBox",      "Notification",      "Message",      "InfiniteScroll",    ].indexOf(componentName) === -1  ) {    installTemplate.push(      render(INSTALL_COMPONENT_TEMPLATE, {        name: componentName,        component: name,      })    );  }  if (componentName !== "Loading") listTemplate.push(`  ${componentName}`);});var template = render(MAIN_TEMPLATE, {  include: includeComponentTemplate.join(endOfLine),  install: installTemplate.join("," + endOfLine),  version: process.env.VERSION || require("../../package.json").version,  list: listTemplate.join("," + endOfLine),});// 后果输入到src/index.js中fs.writeFileSync(OUTPUT_PATH, template);console.log("[build entry] DONE:", OUTPUT_PATH);

其实就是下面说的,依据components.json,生成src/index.js文件。

build/bin/i18n.js

依据 examples/i18n/page.json 和模版,生成不同语言的 demo,也就是官网 demo 展现国际化的解决。

ElementUI官网的国际化根据的模版是examples/pages/template,依据不同的语言,别离生成不同的文件:

这外面都是.tpl文件,每个文件对应一个模版,而且每个tpl文件又都是合乎SFC标准的Vue文件。

咱们轻易关上一个文件:

export default {  data() {    return {      lang: this.$route.meta.lang,      navsData: [        {          path: "/design",          name: "<%= 1 >",        },        {          path: "/nav",          name: "<%= 2 >",        },      ],    };  },};

外面都有数字标示了须要国际化解决的中央。

首页所有国际化相干的字段对应关系存储在examples/i18n/page.json中:

最终官网展现进去的就是通过下面国际化解决后的页面:

反对切换不同语言。

绕了一圈,回到主题:build/bin/i18n.js帮咱们做了什么呢?

咱们思考一个问题:首页的展现是如何做到依据不同语言,生成不同的vue文件呢?

这就是build/bin/i18n.js帮咱们做的事件。

来看下对应的源码:

"use strict";var fs = require("fs");var path = require("path");var langConfig = require("../../examples/i18n/page.json");langConfig.forEach((lang) => {  try {    fs.statSync(path.resolve(__dirname, `../../examples/pages/${lang.lang}`));  } catch (e) {    fs.mkdirSync(path.resolve(__dirname, `../../examples/pages/${lang.lang}`));  }  Object.keys(lang.pages).forEach((page) => {    var templatePath = path.resolve(      __dirname,      `../../examples/pages/template/${page}.tpl`    );    var outputPath = path.resolve(      __dirname,      `../../examples/pages/${lang.lang}/${page}.vue`    );    var content = fs.readFileSync(templatePath, "utf8");    var pairs = lang.pages[page];    Object.keys(pairs).forEach((key) => {      content = content.replace(        new RegExp(`<%=\\s*${key}\\s*>`, "g"),        pairs[key]      );    });    fs.writeFileSync(outputPath, content);  });});

解决流程也很简略:遍历examples/i18n/page.json,依据不同的数据结构把tpl文件的标记位,通过正则匹配进去,并替换成本人事后设定好的字段。

这样官网首页的国际化就实现了。

build/bin/version.js

依据package.json中的version,生成examples/versions.json,对应就是残缺的版本列表

build:theme

解决款式相干。

"build:theme": "node build/bin/gen-cssfile && gulp build --gulpfile packages/theme-chalk/gulpfile.js && cp-cli packages/theme-chalk/lib lib/theme-chalk",

同样这一条也关联了多个操作,咱们拆开来看。

build/bin/gen-cssfile

这一步是依据components.json,生成package/theme-chalk/index.scss文件,把所有组件的款式都导入到index.scss

其实是做了一个自动化导入操作,前面每次新增组件,就不必手动去引入新增组件的款式了。

gulp build --gulpfile packages/theme-chalk/gulpfile.js

咱们都晓得ElementUI在应用时有两种引入形式:

  • 全局引入
import Vue from "vue";import ElementUI from "element-ui";import "element-ui/lib/theme-chalk/index.css";import App from "./App.vue";Vue.use(ElementUI);new Vue({  el: "#app",  render: (h) => h(App),});
  • 按需引入
import Vue from "vue";import { Pagination, Dropdown } from "element-ui";import App from "./App.vue";Vue.use(Pagination);Vue.use(Dropdown);new Vue({  el: "#app",  render: (h) => h(App),});

对应两种引入形式,Element在打包时对应的也有两种计划。

具体如下:将packages/theme-chalk下的所有scss文件编译为css,当你须要全局引入时,就去引入index.scss文件;当你按需引入时,引入对应的组件scss文件即可。

这其中有一点,咱们须要思考下:如何把packages/theme-chalk下的所有scss文件编译为css

在平时的开发中,咱们打包、压缩之类的工作往往都会交给webpack去解决,然而,针对下面这个问题,咱们如果采纳gulp基于工作流去解决会更加不便。

gulp相干的解决就在packages/theme-chalk/gulpfile.js中:

"use strict";const { series, src, dest } = require("gulp");const sass = require("gulp-sass"); // 编译gulp工具const autoprefixer = require("gulp-autoprefixer"); // 增加厂商前缀const cssmin = require("gulp-cssmin"); // 压缩cssfunction compile() {  return src("./src/*.scss") // src下的所有scss文件    .pipe(sass.sync()) // 把scss文件编译成css    .pipe(      autoprefixer({        // 基于指标浏览器版本,增加厂商前缀        browsers: ["ie > 9", "last 2 versions"],        cascade: false,      })    )    .pipe(cssmin()) // 压缩css    .pipe(dest("./lib")); // 输入到lib下}function copyfont() {  return src("./src/fonts/**") // 读取src/fonts下的所有文件    .pipe(cssmin())    .pipe(dest("./lib/fonts")); // 输入到lib/fonts下}exports.build = series(compile, copyfont);

通过解决,最终就会打包出对应的款式文件

cp-cli packages/theme-chalk/lib lib/theme-chalk
cp-cli 是一个跨平台的copy工具,和CopyWebpackPlugin相似

这里就是复制文件到lib/theme-chalk下。

下面提到过屡次components.json,上面就来理解下。

components.json

这个文件其实就是记录了组件的门路,在自动化生成文件以及入口时会用到:

{  "pagination": "./packages/pagination/index.js",  "dialog": "./packages/dialog/index.js",  "autocomplete": "./packages/autocomplete/index.js",  // ...  "avatar": "./packages/avatar/index.js",  "drawer": "./packages/drawer/index.js",  "popconfirm": "./packages/popconfirm/index.js"}

packages

寄存着组件库的源码和组件款式文件。

这里以Alert组件为例做下阐明:

Alert 文件夹


这里main.vue对应就是组件源码,而index.js就是入口文件:

import Alert from "./src/main";/* istanbul ignore next */Alert.install = function (Vue) {  Vue.component(Alert.name, Alert);};export default Alert;

引入组件,而后为组件提供install办法,让Vue能够通过Vue.use(Alert)去应用。

对于install能够看官网文档

packages/theme-chalk

这外面寄存的就是所有组件相干的款式,下面也曾经做过阐明了,外面有index.scss(用于全局引入时导出所有组件款式)和其余每个组件对应的scss文件(用于按需引入时导出对应的组件款式)

src

说了半天,终于绕到了src文件夹。

下面的packages文件夹是离开去解决每个组件,而src的作用就是把所有的组件做一个对立解决,同时蕴含自定义指令、我的项目整体入口、组件国际化、组件 mixins、动画的封装和公共办法。

咱们次要来看下入口文件,也就是src/index.js

/* Automatically generated by './build/bin/build-entry.js' */// 导入了packages下的所有组件import Pagination from "../packages/pagination/index.js";import Dialog from "../packages/dialog/index.js";import Autocomplete from "../packages/autocomplete/index.js";// ...const components = [  Pagination,  Dialog,  Autocomplete,  // ...];// 提供了install办法,帮咱们挂载了一些组件与变量const install = function (Vue, opts = {}) {  locale.use(opts.locale);  locale.i18n(opts.i18n);  // 把所有的组件注册到Vue下面  components.forEach((component) => {    Vue.component(component.name, component);  });  Vue.use(InfiniteScroll);  Vue.use(Loading.directive);  Vue.prototype.$ELEMENT = {    size: opts.size || "",    zIndex: opts.zIndex || 2000,  };  Vue.prototype.$loading = Loading.service;  Vue.prototype.$msgbox = MessageBox;  Vue.prototype.$alert = MessageBox.alert;  Vue.prototype.$confirm = MessageBox.confirm;  Vue.prototype.$prompt = MessageBox.prompt;  Vue.prototype.$notify = Notification;  Vue.prototype.$message = Message;};/* istanbul ignore if */if (typeof window !== "undefined" && window.Vue) {  install(window.Vue);}// 导出版本号、install办法(插件)、以及一些性能比方国际化性能export default {  version: "2.13.2",  locale: locale.use,  i18n: locale.i18n,  install,  Pagination,  Dialog,  Autocomplete,  // ...};

文件结尾的:

/* Automatically generated by './build/bin/build-entry.js' */

其实在下面的scriptsbuild/bin/build-entry.js中咱们曾经提到过:src/index.js是由build-entry脚本主动生成的。

这个文件次要做下以下事件:

  • 导入了 packages 下的所有组件
  • 对外裸露了install办法,把所有的组件注册到Vue下面,并在Vue原型上挂载了一些全局变量和办法
  • 最终将install办法、变量、办法导出

examples

寄存了 ElementUI的组件示例。

其实从目录构造,咱们不难看出这是一个残缺独立的Vue我的项目。次要用于官网文档的展现:

这里咱们次要关注下docs文件夹:

Element官网反对 4 种语言,docs一共有 4 个文件夹,每个文件夹外面的内容根本是一样的。

咱们能够看到外面全部都是md文档,而每一个md文档,别离对应着官网组件的展现页面。

其实当初各大支流组件库文档都是用采纳md编写。

咱们下面大抵理解了源码的几个次要文件目录,然而都比拟扩散。上面咱们从构建指令到新建组件、打包流程、公布组件残缺的看一下构建流程。

构建流程梳理

构建指令(Makefile)

平时咱们都习惯将我的项目罕用的脚本放在package.json中的scripts中。但ElementUI还应用了Makefile文件(因为文件内容较多,这里就选取了几个做下阐明):

.PHONY: dist testdefault: help# build all themebuild-theme:    npm run build:themeinstall:    npm installinstall-cn:    npm install --registry=http://registry.npm.taobao.orgdev:    npm run devplay:    npm run dev:playnew:    node build/bin/new.js $(filter-out $@,$(MAKECMDGOALS))dist: install    npm run distdeploy:    @npm run deploypub:    npm run pubtest:    npm run test:watch// Tip:// make new <component-name> [中文]// 1、将新建组件增加到components.json// 2、增加到index.scss// 3、增加到element-ui.d.ts// 4、创立package// 5、增加到nav.config.json

我是第一次见,所以就去Google下,网上对Makefile对定义大略是这样:

Makefile 是一个实用于 C/C++ 的工具,较早作为工程化工具呈现在 UNIX 零碎中, 通过 make 命令来执行一系列的编译和连贯操作。在领有 make 环境的目录下, 如果存在一个 Makefile 文件。 那么输出 make 命令将会执行 Makefile 文件中的某个指标命令。

这里我以make install为例简要阐明下执行流程:

  • 执行 make 命令, 在该目录下找到 Makefile 文件。
  • 找到 Makefile 文件中对应命令行参数的 install 指标。这里的指标就是 npm install

构建入口文件

咱们看下scripts中的dev指令:

"dev":"npm run bootstrap &&npm run build:file &&cross-env NODE_ENV=developmentwebpack-dev-server --config build/webpack.demo.js &node build/bin/template.js",

首先npm run bootstrap是用来装置依赖的。

npm run build:file在后面也有提到,次要用来自动化生成一些文件。次要是node build/bin/build-entry.js,用于生成Element的入口js:先是读取根目录的components.json,这个json文件维护着Element所有的组件门路映射关系,键为组件名,值为组件源码的入口文件;而后遍历键值,将所有组件进行import,对外裸露install办法,把所有import的组件通过Vue.component(name, component)形式注册为全局组件,并且把一些弹窗类的组件挂载到Vue的原型链上(这个在下面介绍scripts相干脚本时有具体阐明)。

在生成了入口文件的src/index.js之后就会运行webpack-dev-server

webpack-dev-server --config build/webpack.demo.js

这个后面也提过,用于跑Element官网的根底配置。

新建组件

下面咱们提到了,Element中还用了makefile为咱们编写了一些额定的脚本。

这里重点说一下 make new <component-name> [中文] 这个命令。

当运行这个命令的时候,其实运行的是 node build/bin/new.js

build/bin/new.js比较简单,备注也很清晰,它帮咱们做了上面几件事:

1、新建的组件增加到components.json

2、在packages/theme-chalk/src下新建对应到组件scss文件,并增加到packages/theme-chalk/src/index.scss

3、增加到 element-ui.d.ts,也就是对应的类型申明文件

4、创立package(咱们下面有提到组件相干的源码都在package目录下寄存)

5、增加到nav.config.json(也就是官网组件左侧的菜单)

打包流程剖析

ElementUI打包执行的脚本是:

"dist":  "npm run clean &&   npm run build:file &&   npm run lint &&   webpack --config build/webpack.conf.js && webpack --config build/webpack.common.js && webpack --config build/webpack.component.js &&   npm run build:utils &&   npm run build:umd &&   npm run build:theme",

上面咱们一一来进行剖析:

npm run clean(清理文件)

"clean": "rimraf lib && rimraf packages/*/lib && rimraf test/**/coverage",

删除之前打包生成文件。

npm run build:file(生成入口文件)

依据components.json生成入口文件src/index.js,以及i18n相干文件。这个在下面曾经做过剖析,这里就不再开展进行阐明。

npm run lint(代码查看)

"lint": "eslint src/**/* test/**/* packages/**/* build/**/* --quiet",

我的项目eslint检测,这也是当初我的项目必备的。

文件打包相干

webpack --config build/webpack.conf.js &&webpack --config build/webpack.common.js &&webpack --config build/webpack.component.js
build/webpack.conf.js

生成umd格局的js文件(index.js)

build/webpack.common.js

生成commonjs格局的js文件(element-ui.common.js),require时默认加载的是这个文件。

build/webpack.component.js

components.json为入口,将每一个组件打包生成一个文件,用于按需加载。

npm run build:utils(转译工具办法)

"build:utils": "cross-env BABEL_ENV=utils babel src --out-dir lib --ignore src/index.js",

src目录下的除了index.js入口文件外的其余文件通过babel转译,而后挪动到lib文件夹下。

npm run build:umd(语言包)

"build:umd": "node build/bin/build-locale.js",

生成umd模块的语言包。

npm run build:theme(生成款式文件)

"build:theme": "node build/bin/gen-cssfile && gulp build --gulpfile packages/theme-chalk/gulpfile.js && cp-cli packages/theme-chalk/lib lib/theme-chalk",

依据components.json,生成package/theme-chalk/index.scss。用gulp构建工具,编译scss、压缩、输入csslib目录。

最初用一张图来形容上述整个打包流程:

公布流程

打包实现,紧跟着就是代码的公布了。Element中公布次要是用shell脚本实现的。

Element公布一共波及三个局部:

1、git 公布

2、npm 公布

3、官网公布

公布对应的脚本是:

"pub":  "npm run bootstrap &&   sh build/git-release.sh &&   sh build/release.sh &&   node build/bin/gen-indices.js &&   sh build/deploy-faas.sh",

sh build/git-release.sh(代码冲突检测)

运行 git-release.sh 进行git抵触的检测,这里次要是检测dev分支是否抵触,因为Element是在dev分支进行开发的。

#!/usr/bin/env sh# 切换至dev分支git checkout dev# 检测本地和暂存区是否还有未提交的文件if test -n "$(git status --porcelain)"; then  echo 'Unclean working tree. Commit or stash changes first.' >&2;  exit 128;fi# 检测本地分支是否有误if ! git fetch --quiet 2>/dev/null; then  echo 'There was a problem fetching your branch. Run `git fetch` to see more...' >&2;  exit 128;fi# 检测本地分支是否落后近程分支if test "0" != "$(git rev-list --count --left-only @'{u}'...HEAD)"; then  echo 'Remote history differ. Please pull changes.' >&2;  exit 128;fi# 通过以上查看,示意代码无抵触echo 'No conflicts.' >&2;

公布 npm && 官网更新

dev分支代码检测没有抵触,接下来就会执行release.sh脚本,合并dev分支到master、更新版本号、推送代码到近程仓库并公布到npm(npm publish)。

官网更新大抵就是:将动态资源生成到examples/element-ui目录下,而后放到gh-pages分支,这样就能通过github pages的形式拜访。

到这里ElementUI的残缺构建流程就剖析完了。

ui 组件库搭建指北

通过对ElementUI源码文件和构建流程的剖析,上面咱们能够总结一下搭建一个齐备的 ui 组件库都须要做什么工作。

目录构造

目录构造对于大型项目是尤其重要的,正当清晰的构造对于前期的开发和扩大都是很有意义的。ui组件库的目录构造,我感觉ElementUI的就很不错:

|-- Element    |-- .babelrc                           // babel相干配置    |-- .eslintignore    |-- .eslintrc                          // eslint相干配置    |-- .gitattributes    |-- .gitignore    |-- .travis.yml                        // ci配置    |-- CHANGELOG.en-US.md    |-- CHANGELOG.es.md    |-- CHANGELOG.fr-FR.md    |-- CHANGELOG.zh-CN.md                 // 版本改变阐明    |-- FAQ.md                             // 常见问题QA    |-- LICENSE                            // 版权协定相干    |-- Makefile                           // 脚本汇合(工程化编译)    |-- README.md                          // 我的项目阐明文档    |-- components.json                    // 组件配置文件    |-- element_logo.svg    |-- package.json    |-- yarn.lock    |-- .github                            // 贡献者、issue、PR模版    |   |-- CONTRIBUTING.en-US.md    |   |-- CONTRIBUTING.es.md    |   |-- CONTRIBUTING.fr-FR.md    |   |-- CONTRIBUTING.zh-CN.md    |   |-- ISSUE_TEMPLATE.md    |   |-- PULL_REQUEST_TEMPLATE.md    |   |-- stale.yml    |-- build                              // 打包    |-- examples                           // 示例代码    |-- packages                           // 组件源码    |-- src                                // 入口文件以及各种辅助文件    |-- test                               // 单元测试文件    |-- types                              // 类型申明

组件开发

参考大多数 UI 组件库的做法,能够将 examples 下的示例代码组织起来并裸露一个入口,应用 webpack 配置一个 dev-server,后续对组件的调试、运行都在此 dev-server 下进行。

单元测试

UI 组件作为高度形象的根底公共组件,编写单元测试是很有必要的。合格的单元测试也是一个成熟的开源我的项目必备的。

打包

对于打包后的文件,对立放在 lib 目录下,同时记得要在 .gitignore 中加上 lib 目录,防止将打包后果提交到代码库中。

同时针对引入形式的不同,要提供全局引入(UMD)和按需加载两种模式的包。

文档

组件库的文档个别都是对外可拜访的,因而须要部署到服务器上,同时也需具备本地预览的性能。

公布

组件库的某个版本实现开发工作后,须要将包公布到 npm 上。公布流程:

  • 执行测试用例
  • 打包构建
  • 更新版本号
  • npm 包公布
  • 打 tag
  • 自动化部署

保护

公布后须要日常保护之前老版本,个别须要留神一下几点:

  • issue(bug 修复)
  • pull request(代码 pr)
  • CHANGELOG.md(版本改变记录)
  • CONTRIBUTING.md(我的项目贡献者及标准)

参考

  • https://segmentfault.com/a/11...
  • https://zhuanlan.zhihu.com/p/...

❤️ 爱心三连击

1.如果感觉这篇文章还不错,来个分享、点赞、在看三连吧,让更多的人也看到~

2.关注公众号前端森林,定期为你推送陈腐干货好文。

3.非凡阶段,带好口罩,做好集体防护。