10秒鐘構建你自己的”造輪子”工廠! 2019年github/npm工程化協作開發棧最佳實踐

靈魂拷問:你有發佈過npm包嗎?

發起過多人協作的github開源項目嗎?

據統計,70%的前端工程師從來沒發佈過npm包

對於初中級前端,維護開源項目是比較遙遠的,

而前端工具的變化太快,高級前端也難以確定自己能寫出開發棧的最佳實踐,只能不斷花時間摸索。

image

發起一個github/npm工程協作項目,門檻太高了!!

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

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

這四樣工具的配置,是每個github項目都會用上的。另外,gitignore配置editconfigreadmelisence。。。也是必不可缺的。

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

這樣的時間成本,可以直接勸退大多數人。

然而,有的開發者,我們仰視的“神”,一年可以發上百個github/npm項目,其中更有幾千上萬的Star的大項目。

他們是正常人嗎? 他們如何這樣批量的造輪子的?

今天,這篇文章,讓你第一次,擁有“神”的能力。

文章不是很長,但對你的前端生涯可能產生決定性影響,你會發現你與“神”之間的距離如此之近。

一切配置標準、正式、現代化。從此,你隨手寫的小工具、小函數,可以不斷吸引協作開發者,膨帳成大型協作項目。就像當初的尤雨溪仿寫angular時一樣的起點。


第一部分: 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正版源。

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😘
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章