發起一個github/npm工程協作專案,門檻太高了!!
最基礎的問題,你都要花很久去研究:
- 如何在專案中全線使用es2017程式碼? 答案是babel
- 如何統一所有協作者的程式碼風格? 答案是eslint + prettier
- 如何測試驅動開發,讓專案更健壯? 答案是jest
- 如何持續化整合,方便更多協作者參與專案? 答案是circleci
這四樣工具的配置,是每個github專案都會用上的。另外,gitignore配置、editconfig、readme、lisence。。。也是必不可缺的。
你可能需要花數天時間去研究文件、數天時間去做基礎配置。
這樣的時間成本,可以直接勸退大多數人。
但,假如幾秒鐘,就可以按需求配置好這一切呢?
你可以先來體驗一下“輪子工廠”,在命令列輸入:
npx lunz myapp
一路回車,然後試一試yarn lint
,yarn test
,yarn 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: 輸入`jest`
git repository: 輸入你的英文名加上包名,例如`wanthering/npmdev`
keywords: 自己瞎寫一個,不填也行
author: 你的英文名,例如`wanthering`
license: 輸入`MIT`
在package.json中新增files欄位,使npm發包時只發布dist
...
"files": ["dist"],
...
之前不是建立了.editorconfig
、LICENSE
、circle.yml
、.gitignore
、README.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
下的prettier
和plugin: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
當檔案操作處理完之後,我們還需要做如下操作:
- 初始化git
- 安裝package裡的依賴
- 輸出使用指南
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...
一起加入,構造更完美的最佳實佳!
- 點選右上角的Fork按鈕。
- 新建一個分支:
git checkout -b my-new-feature
- 上報你的更新:
git commit -am 'Add some feature'
- 分支上傳雲端:
git push origin my-new-feature
- 提交
pull request
?