10秒鐘構建你本身的」造輪子」工廠! 2019年github/npm工程化協做開發棧最佳實踐

發起一個github/npm工程協做項目,門檻過高了!!javascript

最基礎的問題,你都要花好久去研究:css

  • 如何在項目中全線使用es2017代碼? 答案是babel
  • 如何統一全部協做者的代碼風格? 答案是eslint + prettier
  • 如何測試驅動開發,讓項目更健壯? 答案是jest
  • 如何持續化集成,方便更多協做者參與項目? 答案是circleci

這四樣工具的配置,是每一個github項目都會用上的。另外,gitignore配置、editconfig、readme、lisence。。。也是必不可缺的。前端

你可能須要花數天時間去研究文檔、數天時間去作基礎配置。vue

這樣的時間成本,能夠直接勸退大多數人。java

但,假如幾秒鐘,就能夠按需求配置好這一切呢?node

你能夠先來體驗一下「輪子工廠」,在命令行輸入:webpack

npx lunz myapp

一路回車,而後試一試yarn lintyarn testyarn build命令git


第一部分: 2019年github + npm工程化協做開發棧最佳實踐es6

第二部分: 使用腳手架,10秒鐘構建可自由配置的開發棧。github


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: 輸入`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": "jest",
    "lint": "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": "jest",
    "lint": "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字段爲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{ %>babel src --out-dir dist<% } %>"<% if(test){ %>,
    "test": "jest"<% } %><% if(linter){ %>,
    "lint": "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正版源。

image

結語:

如今,你有什麼想法,只須要隨時隨刻 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😘
相關文章
相關標籤/搜索