乐趣区

10秒钟构建你自己的造轮子工厂-2019年githubnpm工程化协作开发栈最佳实践

灵魂拷问:你有发布过 npm 包吗?

发起过多人协作的 github 开源项目吗?

据统计,70% 的前端工程师从来没发布过 npm 包

对于初中级前端,维护开源项目是比较遥远的,

而前端工具的变化太快,高级前端也难以确定自己能写出开发栈的最佳实践,只能不断花时间摸索。

发起一个 github/npm 工程协作项目,门槛太高了!!

最基础的问题,你都要花很久去研究:

  • 如何在项目中全线使用 es2017 代码?答案是 babel
  • 如何统一所有协作者的代码风格?答案是 eslint + prettier
  • 如何测试驱动开发,让项目更健壮?答案是 jest
  • 如何持续化集成,方便更多协作者参与项目?答案是 circleci

这四样工具的配置,是每个 github 项目都会用上的。另外,gitignore 配置 editconfigreadmelisence。。。也是必不可缺的。

你可能需要花数天时间去研究文档、数天时间去做基础配置。

这样的时间成本,可以直接劝退大多数人。

然而,有的开发者,我们仰视的“神”,一年可以发上百个 github/npm 项目,其中更有几千上万的 Star 的大项目。

他们是正常人吗?他们如何这样批量的造轮子的?

今天,这篇文章,让你第一次,拥有“神”的能力。

文章不是很长,但对你的前端生涯可能产生决定性影响,你会发现你与“神”之间的距离如此之近。

一切配置标准、正式、现代化。从此,你随手写的小工具、小函数,可以不断吸引协作开发者,膨帐成大型协作项目。就像当初的尤雨溪仿写 angular 时一样的起点。

你可以先来体验一下“轮子工厂”,在命令行输入:

npx lunz myapp

一路回车,然后试一试 yarn lintyarn testyarn build 命令


第一部分:2019 年 github + npm 工程化协作开发栈最佳实践

第二部分:使用脚手架,10 秒钟构建可自由配置的开发栈。


2019 年 github + npm 工程化协作开发栈最佳实践

我们将花半小时实战撸一个包含 package.json, babel, jest, eslint, prettify, gitignore, readme,lisence 的标准的 用于 github 工程协作的 npm 包开发栈

如果能实际操作,就实际操作。

如果不能实际操作,请在 bash 下输入 npx lunz npmdev 获得同样的效果。

1. 新建文件夹

mkdir npmdev && cd npmdev

2. 初始化 package.json

npm init
package name:回车

version: 回车

description: 自己瞎写一个,不填也行

entry point:  输入 `dist/index.js`

test command: 输入 `npx jest`

git repository:输入你的英文名加上包名,例如 `wanthering/npmdev`

keywords: 自己瞎写一个,不填也行

author: 你的英文名,例如 `wanthering`

license: 输入 `MIT`

在 package.json 中添加 files 字段,使 npm 发包时只发布 dist

  ...
  "files": ["dist"],
  ...

之前不是创建了 .editorconfigLICENSEcircle.yml.gitignoreREADME.md 吗,这四个复制过来。

3. 初始化 eslint

npx eslint --init
How would you like to use ESLint? 选第三个

What type of modules does your project use? 
选第一个

Which framework does your project use?
选第三个 None

Where does your code run?
选第二个 Node

How would you like to define a style for your project? 选第一个 popular

Which style guide do you want to follow?
选第一个 standard

What format do you want your config file to be in?
选第一个 javascript

在 package.json 中添加一条 srcipts 命令:

   ...
  "scripts": {
    "test": "npx jest",
    "lint": "npx eslint src/**/*.js test/**/*.js --fix"
  },
  ...

4. 初始化 prettier

为了兼容 eslint,需要安装三个包

yarn add prettier eslint-plugin-prettier eslint-config-prettier -D

在 package.json 中添加 prettier 字段

  ...
  "prettier": {
    "singleQuote": true,
    "semi": false
  },
  ...

在.eslintrc.js 中,修改 extends 字段:

...
  'extends': ['standard',"prettier","plugin:prettier/recommended"],
...

5. 创建源文件

mkdir src && touch src/index.js

src/index.js 中,我们用最简单的 add 函数做示意

const add = (a,b)=>{return a+b}
    export default add

这时命令行输入

yarn lint

这会看到 index.js 自动排齐成了

const add = (a, b) => {return a + b}
export default add

6. 配置 jest 文件

所有的 npm 包,均采用测试驱动开发。

现在流行的框架,无非 jest 和 ava,其它的 mocha 之类的框架已经死在沙滩上了。

我们安装 jest

npm i jest -D

然后根目录下新建一个 test 文件夹,放置进 jest/index.spec.js 文件

mkdir test && touch test/index.spec.js

在 index.spec.js 内写入:

import add from "../src/index.js";
test('add',()=>{expect(add(1,2)).toBe(3)})

配置一下 eslint+jest:

yarn add eslint-plugin-jest -D

在.eslintrc.js 中,更新 env 字段,添加 plugins 字段:

  'env': {
    'es6': true,
    'node': true,
    'jest/globals': true
  },
  'plugins': ['jest'],
 ...

因为需要 jest 中使用 es6 语句,需要添加 babel 支持

yarn add babel-jest @babel/core @babel/preset-env -D

创建一下.babelrc 配置,注意 test 字段,是专门为了转化测试文件的:

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": {"node": 6}
      }
    ]
  ],
  "env": {
    "test": {
      "presets": [
        [
          "@babel/preset-env",
          {
            "targets": {"node": "current"}
          }
        ]
      ]
    }
  }
}

好,跑一下 yarn lint,以及 yarn test

yarn lint

yarn test

构建打包

比起使用 babel 转码 (安装@babel/cli,再调用npx babel src --out-dir dist),我更倾向于使用bili 进行打包。

yarn add bili -D

然后在 package.json 的 script 中添加

  "scripts": {
    "test": "npx jest",
    "lint": "npx eslint src/**/*.js test/**/*.js --fix",
    "build": "bili"
  },

.gitignore

创建 .gitignore,复制以下内容到文件里

node_modules
.DS_Store
.idea
*.log
dist
output
examples/*/yarn.lock

.editorconfig

创建.editorconfig,复制以下内容到文件里

root = true

[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

[*.md]
trim_trailing_whitespace = false

circle.yml

创建 circle.yml,复制以下内容到文件内

version: 2
jobs:
  build:
    working_directory: ~/project
    docker:
      - image: circleci/node:latest
    branches:
      ignore:
        - gh-pages # list of branches to ignore
        - /release\/.*/ # or ignore regexes
    steps:
      - checkout
      - restore_cache:
          key: dependency-cache-{{checksum "yarn.lock"}}
      - run:
          name: install dependences
          command: yarn install
      - save_cache:
          key: dependency-cache-{{checksum "yarn.lock"}}
          paths:
            - ./node_modules
      - run:
          name: test
          command: yarn test

README.md

创建 README.md,复制以下内容到文件内

# npm-dev

> my laudable project

好了,现在我们的 用于 github 工程协作的 npm 包开发栈 已经完成了,相信我,你不会想再配置一次。

这个项目告一段落。

事实上,这个 npm 包用 npm publish 发布出去,人们在安装它之后,可以作为 add 函数在项目里使用。

使用脚手架,10 秒钟构建可自由配置的开发栈。

同样,这一章节如果没时间实际操作,请输入

git clone https://github.com/wanthering/lunz.git

当你开启新项目,复制粘贴以前的配置和目录结构,浪费时间且容易出错。

package.json、webpack、jest、git、eslint、circleci、prettify、babel、gitigonre、editconfig、readme 的强势劝退组合,让你无路可走。

所以有了 vue-cli,非常强大的脚手架工具,但你想自定义自己的脚手架,你必须学透了 vue-cli。

以及 yeoman,配置贼麻烦,最智障的前端工具,谁用谁 sb。

还有人求助于 docker,

有幸,一位来自成都的宝藏少年 egoist 开发了前端工具 SAO.js。

SAO 背景不错,是 nuxt.js 的官方脚手架。

作为 vue 的亲弟弟 nuxt,不用 vue-cli 反而用 sao.js,你懂意思吧?

因为爽!!!!!!!!

因为,一旦你学会批量构建 npm 包,未来将可以把精力集中在“造轮子”上。

新建 sao.js

全局安装

npm i sao -g

快速创建 sao 模板

sao generator sao-npm-dev

一路回车到底

ok,当前目录下出现了一个 sao-npm-dev

打开看一下:

├── .editorconfig
├── .git
├── .gitattributes
├── .gitignore
├── LICENSE
├── README.md
├── circle.yml
├── package.json
├── saofile.js
├── template
│   ├── .editorconfig
│   ├── .gitattributes
│   ├── LICENSE
│   ├── README.md
│   └── gitignore
├── test
│   └── test.js
└── yarn.lock

别管其它文件,都是用于 github 工程协作的文件。

有用的只有两个:template文件夹,和saofile.js

template 文件夹删空,我们要放自己的文件。

生成 SAO 脚手架

好,把 npmdev 整个文件夹内的内容,除了 node_modules/、package-lock.json 和 dist/,全部拷贝到清空的 sao-npm-dev/template/ 文件夹下

现在的 sao-npm-dev/template 文件夹结构如下:

├── template
│   ├── .babelrc
│   ├── .editorconfig
│   ├── .eslintrc.js
│   ├── .gitignore
│   ├── LICENSE
│   ├── README.md
│   ├── circle.yml
│   ├── package.json
│   ├── src
│   │   └── index.js
│   ├── test
│   │   └── index.spec.js
│   └── yarn.lock

配置文件改名

模板文件中.eslint.js .babelrc .gitignore package.json,很容易造成配置冲突,我们先改名使它们失效:

mv .eslintrc.js _.eslintrc.js

mv .babelrc _.babelrc

mv .gitignore _gitignore

mv package.json _package.json

配置 saofile.js

现在所见的 saofile,由三部分组成:prompts, actions, completed。

分别表示:询问弹窗、自动执行任务、执行任务后操作。

大家可以回忆一下 vue-cli 的创建流程,基本上也是这三个步骤。

弹窗询问的,即是我们 用于 github 工程协作的 npm 包开发栈 每次开发时的变量,有哪些呢?

我来列一张表:

字段 输入方式 可选值 意义
name input 默认为文件夹名 项目名称
description input 默认为 my xxx project 项目简介
author input 默认为 gituser 作者名
features checkbox eslint 和 prettier 安装插件
test confirm yes 和 no 是否测试
build choose babel 和 bili 选择打包方式
pm choose npm 和 yarn 包管理器

根据这张表,我们修改一下 saofile.js 中的 prompts,并且新增一个 templateData(){},用于向 template 中引入其它变量

   prompts() {
    return [
      {
        name: 'name',
        message: 'What is the name of the new project',
        default: this.outFolder
      },
      {
        name: 'description',
        message: 'How would you descripe the new project',
        default: `my ${superb()} project`
      },
      {
        name: 'author',
        message: 'What is your GitHub username',
        default: this.gitUser.username || this.gitUser.name,
        store: true
      },
      {
        name: 'features',
        message: 'Choose features to install',
        type: 'checkbox',
        choices: [
          {
            name: 'Linter / Formatter',
            value: 'linter'
          },
          {
            name: 'Prettier',
            value: 'prettier'
          }
        ],
        default: ['linter', 'prettier']
      },
      {
        name: 'test',
        message: 'Use jest as test framework?',
        type: 'confirm',
        default: true
      },
      {
        name: 'build',
        message: "How to bundle your Files?",
        choices: ['bili', 'babel'],
        type: 'list',
        default: 'bili'
      },
      {
        name: 'pm',
        message: 'Choose a package manager',
        choices: ['npm', 'yarn'],
        type: 'list',
        default: 'yarn'
      }
    ]
  },
  templateData() {const linter = this.answers.features.includes('linter')
    const prettier = this.answers.features.includes('prettier')
    return {linter, prettier}
  },

先把 saofile 放下,我们去修改一下 template 文件,使 template 中的文件可以应用这些变量

修改 template/ 中的变量

template 下的文件,引入变量的方式是 ejs 方式,不熟悉的可以看一看 ejs 官方页面,非常简单的一个模板引擎

现在我们一个一个审视文件,看哪些文件需要根据变量变动。

1. src/index.js

无需变动

2. test/index.spec.js

如果 test 为 false,则文件无需加载。test 为 true, 则加载文件。

3. .editorconfig

无需改动

4. _.gitignore

无需改动

5. _.babelrc

如果 build 采用的 babel,或 test 为 true,则导入文件。

并且,如果 test 为 true, 应当开启 env,如下设置文件

_.babelrc

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": {"node": 6}
      }
    ]
  ]<% if(test) { %>,
  "env": {
    "test": {
      "presets": [
        [
          "@babel/preset-env",
          {
            "targets": {"node": "current"}
          }
        ]
      ]
    }
  }<% } %>
}

6. _.eslintrc.js

在打开 test 的情况下,加载 env 下的 jest/globals 及设置 plugins 下的jest

在开启 prettier 的情况下,加载 extends 下的 prettierplugin:prettier/recommend

所以文件应当这样改写

_.eslintrc.js

module.exports = {
  'env': {
    'es6': true,
    'node': true<% if(test) { %>,
    'jest/globals': true<% } %>
  }<% if(test) { %>,
 'plugins': ['jest']<% } %>,
  'extends': ['standard'<% if(prettier) {%>,'prettier','plugin:prettier/recommended'<%} %>],
  'globals': {
    'Atomics': 'readonly',
    'SharedArrayBuffer': 'readonly'
  },
  'parserOptions': {
    'ecmaVersion': 2018,
    'sourceType': 'module'
  }
}

7. _package.json

name 字段,加载 name 变量
description 字段,加载 description 变量
author 字段,加载 author 变量

bugs,homepage,url 跟据 author 和 name 设置

prettier 为 true 时,设置 prettier 字段,以及 devDependence 加载 eslint-plugin-prettier、eslint-config-prettier 以及 prettier

eslint 为 true 时,加载 eslint 下的其它依赖。

jest 为 true 时,加载 eslint-plugin-jest、babel-jest、@babel/core 和 @babel/preset-env,且设置 scripts 下的 lint 语句

build 为 bili 时,设置 scripts 下的 build 字段为 bili

build 为 babel 时,设置 scripts 下的 build 字段为npx babel src --out-dir dist

最后实际的文件为:(注意里面的 ejs 判断语句)

{
  "name": "<%= name %>",
  "version": "1.0.0",
  "description": "<%= description %>",
  "main": "dist/index.js",
  "scripts": {"build": "<% if(build ==='bili') {%>bili<%}else{%>npx babel src --out-dir dist<%} %>"<% if(test){ %>,
    "test": "npx jest"<% } %><% if(linter){ %>,
    "lint": "npx eslint src/**/*.js<% } if(linter && test){%> test/**/*.js<%} if(linter){%> --fix"<%} %>
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/<%= author %>/<%= name %>.git"
  },
  "author": "<%= author %>",
  "license": "MIT",
  "bugs": {"url": "https://github.com/<%= author %>/<%= name %>/issues"}<% if(prettier){ %>,
  "prettier": {
    "singleQuote": true,
    "semi": false
  }<% } %>,
  "homepage": "https://github.com/<%= author %>/<%= name %>#readme",
  "devDependencies": {<% if(build === 'bili'){ %>
    "bili": "^4.7.4"<% } %><% if(build === 'babel'){ %>
    "@babel/cli": "^7.4.4"<% } %><% if(build === 'babel' || test){ %>,
    "@babel/core": "^7.4.4",
    "@babel/preset-env": "^7.4.4"<% } %><% if(test){ %>,
    "babel-jest": "^24.8.0",
    "jest": "^24.8.0"<% } %><% if(linter){ %>,
    "eslint": "^5.16.0",
    "eslint-config-standard": "^12.0.0",
    "eslint-plugin-import": "^2.17.2",
    "eslint-plugin-node": "^9.0.1",
    "eslint-plugin-promise": "^4.1.1",
    "eslint-plugin-standard": "^4.0.0"<% } %><% if(linter && test){ %>,
    "eslint-plugin-jest": "^22.5.1"<% } %><% if (prettier){ %>,
    "prettier": "^1.17.0",
    "eslint-plugin-prettier": "^3.1.0",
    "eslint-config-prettier": "^4.2.0"<% } %>
  }
}

8. circle.yml

判断使用的 lockFile 文件是 yarn.lock 还是 package-lock.json

<% const lockFile = pm === 'yarn' ? 'yarn.lock' : 'package-lock.json' -%>
version: 2
jobs:
  build:
    working_directory: ~/project
    docker:
      - image: circleci/node:latest
    branches:
      ignore:
        - gh-pages # list of branches to ignore
        - /release\/.*/ # or ignore regexes
    steps:
      - checkout
      - restore_cache:
          key: dependency-cache-{{checksum "<%= lockFile %>"}}
      - run:
          name: install dependences
          command: <%= pm %> install
      - save_cache:
          key: dependency-cache-{{checksum "<%= lockFile %>"}}
          paths:
            - ./node_modules
      - run:
          name: test
          command: <%= pm %> test

9. README.md

# <%= name %>

> <%= description %>

填入 name 和 desc 变量。

并跟据 linter、test、build 变量来选择提示命令。

具体文件略。


好,文件的变量导入完成,现在回到 saofile.js:

处理 actions

当我们通过弹窗询问到了变量。

当我们在构建好模板文件,只等变量导入了。

现在就需要通过 saofile.js 中的 actions 进行导入。

把 actions 进行如下改写:

  actions() {
    return [{
      type: 'add',
      files: '**',
      filters: {
        '_.babelrc': this.answers.test || this.answers.build === 'babel',
        '_.eslintrc.js': this.answers.features.includes('linter'),
        'test/**': this.answers.test
      }
    }, {
      type: 'move',
      patterns: {
        '_package.json': 'package.json',
        '_gitignore': '.gitignore',
        '_.eslintrc.js': '.eslintrc.js',
        '_.babelrc': '.babelrc'
      }
    }]
  },

其实很好理解!type:'add'表示将模板文件添加到目标文件夹下,files 表示是所有的,filters 表示以下这三个文件存在的条件。

type:'move'就是改名或移动的意思,将之前加了下划线的四个文件,改回原来的名字。

处理 competed

当文件操作处理完之后,我们还需要做如下操作:

  1. 初始化 git
  2. 安装 package 里的依赖
  3. 输出使用指南
  async completed() {this.gitInit()
    await this.npmInstall({npmClient: this.answers.pm})
    this.showProjectTips()}

跑通测试

SAO 已经帮你写好了测试文件,在 test 文件夹下。

因为我们要测试很多个选项,原来的 sao.mock 和 snapshot 要写很多次。所以我们把它提炼成一个新的函数 verifyPkg()

我们进行一下改写,同时将 package.json、.eslintrc.js 打印在 snapshot 文件中。

import path from 'path'
import test from 'ava'
import sao from 'sao'

const generator = path.join(__dirname, '..')

const verifyPkg = async (t, answers) => {const stream = await sao.mock({ generator}, answers)
  const pkg = await stream.readFile('package.json')
  t.snapshot(stream.fileList, 'Generated files')
  t.snapshot(getPkgFields(pkg), 'package.json')

  if(answers && answers.features.includes('linter')){const lintFile = await stream.readFile('.eslintrc.js')
    t.snapshot(lintFile, '.eslintrc.js')
  }
}

const getPkgFields = (pkg) => {pkg = JSON.parse(pkg)
  delete pkg.description
  return pkg
}

test('defaults', async t => {await verifyPkg(t)
})

test('only bili', async t => {
  await verifyPkg(t,{features: [],
    test: false,
    build: 'bili'
  })
})

test('only babel', async t => {
  await verifyPkg(t,{features: [],
    test: false,
    build: 'babel'
  })
})

test('launch test', async t => {
  await verifyPkg(t,{features: [],
    test: true
  })
})

test('launch linter', async t => {
  await verifyPkg(t,{features: ['linter']
  })
})


test('launch prettier', async t => {
  await verifyPkg(t,{features: ['prettier']
  })
})

ok, 这时候跑一下测试就跑通了
测试文件打印在 snapshots/test.js.md 中,你需要一项一项检查,输入不同变量时候,得到的文件结构和 package.json 以及.eslintrc.js 的内容。

这个时候,整个项目也就完成了。

我们先在 npmjs.com 下注册一个帐号,登录一下 npm login 登录一下。

然后,直接 npm publish 成功之后,就可以使用

sao npm-dev myapp

初始化一个 github 工程化协作开发栈了。

进阶:本地使用 sao.js,发布自定义前端工具

大部分人,不会专门去安装 sao 之后再调用脚手架,而更喜欢使用

npx lunz myapp

那就新添加一个 cli.js 文件

#!/usr/bin/env node
const path = require('path')
const sao = require('sao')

const generator = path.resolve(__dirname, './')
const outDir = path.resolve(process.argv[2] || '.')

console.log(`> Generating lunz in ${outDir}`)

sao({generator, outDir, logLevel: 2})
  .run()
  .catch((err) => {console.trace(err)
    process.exit(1)
  })

通过 sao 函数,可以轻松调用于来 sao 脚手架。

然后,将 package.json 中的 name 改名成你想发布 npm 全局工具名称,比如我创建的是lunz

并且,加入 bin 字段,且修改 files 字段

...
  "bin": "cli.js",
  "files": [
  "cli.js",
  "saofile.js",
  "template"
  ],
  ...

这时,应用一下 npm link 命令,就可以本地模拟出

lunz myapp

的效果了。

如果效果 ok 的话,就可以使用 npm publish 发包。

注意要先登录,登录不上的话可能是因为你处在淘宝源下,请切换到 npm 正版源。

结语:

现在,你有什么想法,只需要随时随刻 npx lunz myapp一下,就可以得到当前最新、最标准、最现代化的 github+npm 工程化实践。

把时间集中花在轮子的构建逻辑上,而不是基础配置上。

与前端之“神”并肩,通过你的经验,让前端的生态更繁荣。

如果实在想研究基础配置,不如帮助我完善这个“轮子工厂”

欢迎大家提交 pull request,交最新的实践整合到项目中

github 地址:https://github.com/wanthering…

一起加入,构造更完美的最佳实佳!

  1. 点击右上角的 Fork 按钮。
  2. 新建一个分支:git checkout -b my-new-feature
  3. 上报你的更新:git commit -am 'Add some feature'
  4. 分支上传云端:git push origin my-new-feature
  5. 提交 pull request????
退出移动版