組內已經有了非常完善以及流暢的開發,釋出流程,平時只需要默默地搬屬於自己的那塊磚就好了,但是每當社群出了新的技術,想嘗試的時候總是欠缺一個“起手式”,可以快速將新的技術給整合到自己的腳手架,或者說工作流中,基於這個目的,想到就開始做了
腳手架對團隊的好處不言而喻,可以通過命令列的方式去快速生成種子檔案,開發以及輸出構建後的程式碼,平時我們只需要開發,而不用跟複雜的編譯過程,搭建服務等流程打交道,另外,還可以將我們需要的node
模組安裝到腳手架內,以後我們只負責開發而不需要安裝龐大的node_module
了,保持目錄的乾淨,甚至腳手架還可以跟後續的持續整合相結合,提供更強大的功能
從零開始搭建腳手架需要一定的前端工程化知識,推薦看webpack指引,裡面涉及了大量前端工程化需要做的事情,事實上我也是從這裡一步一步地往上搭上去的,並最終開發完腳手架qd-cli(音譯:前端-cli,語文不好- -!),開發腳手架本質上還是寫webpack
,用webpack
搭建工作流,並最終可以使用commander將其封裝成命令列工具,這篇文章對commander
介紹得很詳細了,不再重複:基於node.js的腳手架工具開發經歷
本文從以下三個方面做介紹,搭建:如何一步步開發qd-cli(包含了我對前端工程化的瞭解)
,qd-cli的安裝,使用,特性
,搭建過程中遇到的一些坑
搭建
腳手架技術方案選擇
先從簡單地做起,再慢慢地往上堆砌,因此,目前考慮的是只支援移動端專案
,以及vue技術棧
技術方案:工作流的編寫毫無懸念地選擇了webpack,現下最熱門的前端打包工具,webpack首要解決了前端模組化的難題,開箱即用,原生支援es module
,這裡選擇最新的webpack4
,另一方面,將工作流整合成cli
使用commander
開發環境如何搭建
主要考慮以下三個方面:
- 本地伺服器
在開發環境需要有伺服器去啟動並自動重新整理我們的應用,有時甚至期望可以設定代理,便於前後端聯調,可以使用webpack-dev-server,配置很簡單
// webpack.config.js
module.exports = {
// ...
+ devServer: {
+ ...
+ contentBase: cwd('dist'),
+ proxy: { ... }
+ }
}
複製程式碼
- 支援熱過載
在更改程式碼後無需手動重新整理瀏覽器即可預覽效果,快速便捷,即使js的熱過載有點坑,有時需要手動去重新整理,但總體還是利大於弊的
const webpack = require('webpack');
module.exports = {
devServer: {
...
+ hot: true,
contentBase: cwd('dist'),
proxy: { ... }
},
plugins: [
+ new webpack.NamedModulesPlugin(),
+ new webpack.HotModuleReplacementPlugin()
]
}
複製程式碼
- 提供sourcemap
webpack打包後的程式碼報錯後不利於我們去定位錯誤位置,soucemap
可以幫我們準確定位到原始碼的出錯位置
const webpack = require('webpack');
module.exports = {
+ devtool: 'inline-source-map'
devServer: {
hot: true,
contentBase: cwd('dist'),
proxy: { ... }
},
plugins: [
new webpack.NamedModulesPlugin(),
new webpack.HotModuleReplacementPlugin()
]
}
複製程式碼
生產環境配一個最簡單的source-map
就可以了,因為複雜一點的source-map
一般體積都很大
生成環境包太大了怎麼辦,快取問題怎麼處理?
生成環境需要儘可能地優化程式碼的體積,webpack為我們提供了完整的方案,只需一點點的配置
- 程式碼分割
程式碼分割是一件很有必要的操作,在多頁應用中,A,B,C頁面可能同時依賴了大量的第三方庫,將公共庫抽取出來利於瀏覽器做快取,並能有效減少A,B,C頁面的體積
單頁應用也應做程式碼分割,將第三方庫抽取出來,一方面,我們平時需要不斷迭代的部分一般都是業務程式碼,第三方庫的程式碼是不會有變動的,這樣的抽取同樣利於瀏覽器做快取,另一方面,js是單執行緒的,包的體積太大意味著下載變慢,導致js執行緒被掛起
module.exports = {
...
optimization: {
splitChunks: {
cacheGroups: {
// 抽取node_modules中的第三方庫
vendors: {
test: /[\\/]node_modules[\\/]/,
name: "vendors",
chunks: "all"
},
commons: {
name: "commons",
chunks: "initial",
minChunks: 2
}
}
}
}
}
複製程式碼
- 搖樹(tree shaking)
搖樹利用了export,import
的靜態特性,將程式碼中的無用程式碼給刪掉,比如在程式碼中:
import { forEach } from 'lodash-es'
複製程式碼
在最後的打包過程,webpack只會將lodash-es
中的forEach
方法打包進來,其他無用的程式碼不會打包進來,搖樹(tree shaking)
在webpack中的配置非常簡單,如下:
module.exports = {
mode: 'production'
}
複製程式碼
在babel配置裡面需要:
module.exports = {
presets: [
[
'env',
// 啟動tree shaking
{
modules: false
}
],
'stage-2'
]
...
}
複製程式碼
補充:搖樹的概念大概指的是,將我們的程式碼比喻成一棵樹,將無用的程式碼(枯黃的葉子)給搖下來,這裡踩了一個坑,後面補充
- 懶載入
為了提升首屏時間,很多程式碼都可以延遲載入,在webpack體系打包的程式碼中,使用懶載入非常方便
// 方法1
import('./someLazyloadCode').then(_ => {...})
// 方法2, 以下使用方式稱為魔法註釋,可以將最後生成的檔案命名為lazyload,利於我們去分析打包後的程式碼
import(/* webpackChunkName: "lazyload" */ './someLazyloadCode').then(_ => {...})
複製程式碼
在vue
中使用也很方便,可以參考Lazy Loading in Vue using Webpack's Code Splitting
注意,使用懶載入需要新增promise墊片,因為即使是移動端,某些老版本的瀏覽器依然不支援promise,可以使用es6-promise或者promise-polyfill
在對webpack作者Tobias
的採訪中,當被問及能否推薦幾個webpack最佳實踐?作者如是回答:使用按需載入。非常簡單,效果非常好。
- 打雜湊戳
瀏覽器是有快取的,程式碼更改後,如何讓瀏覽器重新載入資源?
傳統的做法是在所有資源連結的後面加時間戳,但這樣做的壞處是隻要更新一個檔案,其他沒有更改的檔案也會因為時間戳的更新而被重新載入,不利於瀏覽器做快取,現在業界比較成熟的做法是給檔名加上雜湊戳,雜湊戳是檔案內容的一一對映,程式碼更改後,雜湊戳也會跟著變,內容沒有更改的檔案雜湊戳也就不會跟著變了
module.exports = {
output: {
filename: isDev ? '[name].js' : '[name].[chunkhash:4].js',
...
},
plugins: [
new Webpack.NamedModulesPlugin(),
]
}
複製程式碼
qd-cli遺留問題,css的雜湊戳跟js的是一樣的,不利於瀏覽器做快取
- 圖片處理
移動端的雪碧圖寬高會帶有小數點導致不好處理,暫不考慮(如果你有好的方案,歡迎提供)。過小的圖片可以轉成base64格式內聯進檔案內,另外,可以使用image-webpack-loader
壓縮圖片,配置如下:
module.exports = {
module: {
rule:
{
test: /\.(png|svga?|jpg|gif)$/,
use: [
{
loader: 'url-loader',
options: {
limit: 8192,
fallback: 'file-loader'
}
}
].concat(isDev ? [] : [
{
loader: 'image-webpack-loader',
options: {
pngquant: {
speed: 4,
quality: '75-90'
},
optipng: {
optimizationLevel: 7
},
mozjpeg: {
quality: 70,
progressive: true
},
gifsicle: {
interlaced: false
}
}
}
])
}
}
}
複製程式碼
- css程式碼抽離
css的抽取可以減少頁面入口的體積,也可以便於css的快取,使用官方推薦的mini-css-extract-plugin
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
module: {
rules: [
{
test: /\.scss$/,
use: [
isDev ? 'vue-style-loader' : MiniCssExtractPlugin.loader,
'css-loader',
{
loader: 'postcss-loader',
options: {
config: {
path: ownDir('lib/config/postcss.config.js')
}
}
},
'sass-loader'
]
}
]
}
}
複製程式碼
- 資源預拉取與資源預載入
webpack4.6+
支援資源預拉取(prefetch)
與資源預載入(preload)
,由於沒有嘗試成功,這裡不做介紹,詳情請看code-splitting
提升webpack的打包效率
相比以前,webpack4
本身就已經快很多了,這裡使用happypack
,happypack
啟動多個程式加速webpack
的打包,程式碼如下:
const os = require('os')
const HappyPack = require('happypack')
const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length })
module.exports = {
plugins: [
new HappyPack({
id: 'eslint',
verbose: false,
loaders: [
...
],
threadPool: happyThreadPool
})
]
}
複製程式碼
社群很多文章會建議使用ddl
打包方式去加速webpack的打包,可以檢視:徹底解決Webpack打包效能問題,由於對這個概念不是很理解,暫不做整合
程式碼目錄結構的規劃(如何支援多頁應用)
為了進一步的編寫腳手架,先定好專案的目錄結構,這樣才會有方向去編寫
+ vue-project
+ src
- index.js
index.art // 每一個xxx.art對應src目錄的xxx.js,開發多頁應用只需要增加這兩個檔案
mock.config.js // 必須:mock服務的配置檔案
config.js // 必須:配置檔案
複製程式碼
使用art-template
作為模板工具,使用art-template
純粹是因為我比較熟悉,使用其他模板也是可以的,每一個xxx.art對應src目錄的xxx.js,開發多頁應用只需要增加對應的兩個檔案就可以了,程式碼的寫作思路是需要entry
入口有xxx.js
,然後plugins
屬性有對應的html-webpack-plugin
,程式碼如下:
const glob = require('globa')
const entry = {}
const htmlPlugins = []
glob.sync(cwd('./src/*.@(js|jsx)')).forEach((filePath) => {
const name = path.basename(filePath, path.extname(filePath))
const artPath = cwd(`${name}.art`)
if (fs.existsSync(artPath)) {
htmlPlugins.push(new HtmlWebpackPlugin({
filename: `${name}.html`,
template: artPath
}))
}
entry[name] = filePath
})
module.exports = {
entry,
plugins: [...].concat(htmlPlugins)
}
複製程式碼
移動端適配方案
目前只考慮移動端專案,說起移動端,首先要考慮的便是適配方案,這裡選擇大漠
大神推薦的vw佈局方案,配置項有點多,這裡不貼了,按照流程走沒遇到什麼問題
技術選型 - vue,es6
因為我對vue比較熟悉,這裡選用了vue,實際上要支援react也只需針對react技術棧做一點點的改動即可,使用vue-loader,參照文件,支援了pug
語法,stylus
, scss
,文件非常的詳細,配置項太多了這裡不貼了,有興趣可以直接看原始碼:qd-cli
支援es6,同時支援async,await,以及裝飾器,這兩款語法都比較實用,社群很多文章都有介紹
module.exports = {
presets: [
[
'env',
// 啟動tree shaking
{
modules: false
}
],
'stage-2'
],
plugins: [
'transform-runtime', // async await
'transform-decorators-legacy' // 裝飾器
]
}
複製程式碼
程式碼規範
使用比較寬鬆的standard規範,以下是eslint的配置檔案
{
extends: [
'standard',
'plugin:vue/essential'
],
rules: {
'no-unused-vars': 1, // 引入未經使用的模組的時候彈出警告而不是報錯中斷編譯,我特別煩no-unused-vars的報錯,特別是在debug的時候- -!
'no-new': 0 // 允許使用new
},
// 不加這一項的話遇到懶載入,async await這樣的特性eslint會報錯
parserOptions: {
parser: 'babel-eslint',
ecmaVersion: 2017,
sourceType: 'module'
},
plugins: [
'vue'
]
}
複製程式碼
mock資料支援
mock資料很有意義,在與後端定好介面後,前端可以通過mock伺服器生成假資料編寫顯示邏輯,這裡使用自己擼的輪子easy-config-mock,很容易繼承到現有的腳手架中,支援mock服務的自動重啟,支援mockjs庫的模擬資料格式,支援使用自定義中介軟體去編寫資料返回邏輯
const EasyConfigMock = require('easy-config-mock');
new EasyConfigMock({
path: cwd('mock.config.js')
})
複製程式碼
mock.config.js
的demo如下:
// mock.config.js
module.exports = {
// common選項不是必須的,可以不用有該選項,內建的配置如下,當然你也可以更改
common: {
// mock服務的預設埠,如果埠被佔用,會自動換一個
port: 8018,
// 如果你想看一下ajax的loading效果,該配置項可以設定介面的返回延遲
timeout: 500,
// 如果你想看一下介面請求失敗的效果,將rate設定成0就可以了,rate取值範圍0~1,代表成功的概率
rate: 1,
// 預設是true,自動開啟mock服務,當然你也可以通過將其設定為false,關閉掉mock服務
mock: true
},
// 普通的api...
'/pkApi/getList': {
code: 0,
'data|5': [{
'uid|1000-99999': 999,
'name': '@cname'
}],
result: true
},
// 中介軟體api(標準的express中介軟體),這裡你可以書寫介面返回邏輯
['/pkApi/getOther'] (req, res, next) {
const id = req.query.id
req.myData = { // 重要! 將返回資料掛載在req.myData
0: {
code: 0,
'test|1-100': 100
},
1: {
code: 1,
'number|+1': 202
},
2:{
code: 2,
'name': '@cname'
}
}[id]
next() // 最後不要忘記手動呼叫一下next,不然介面就暫停處理了!
}
}
複製程式碼
實現原理這裡有介紹:從零開始搭建一個mock服務
專案集支援
專案集的結構可以如下:
+ vue-projects
- project1
- project2
+ project3
+ src
index.js
...
index.art
config.js // 專案配置
mock.config.js // 專案的mock服務
README.md // 專案的說明文件
...
- web_modules // 專案集的公共模組
config.js // 專案集配置
README.md // 專案集的說明文件
複製程式碼
每個小專案都有自己config.js
配置檔案與README.md
說明文件,每個專案集同樣都有自己的config.js
配置檔案與README.md
說明文件,小專案的配置檔案裡的配置可以覆蓋掉專案集的配置,另外,還有webpack_modules
目錄,存放每個專案都可以去使用的公共模組,這樣做的好處是同型別專案可以丟在一起,並且相同的依賴,模組可以丟在web_modules
中,當web_modules
的檔案發生變化,需要發版的時候,後續的持續整合可以統一處理,一鍵全部發版
生成最終配置檔案的程式碼如下:
const R = require('ramda')
const cwd = file => path.resolve(file || '')
const generateConfig = path => {
const cfg = require(cwd(path))
if (typeof cfg === 'function') {
return cfg({})
} else {
return cfg
}
}
module.exports = {
getConfig: R.memoize(_ => {
let config = {}
// 如果是專案集,專案集也會有個config.js
if (fs.existsSync('../config.js')) {
config = R.merge(config, generateConfig('../config'))
}
config = R.merge(config, generateConfig('config.js'))
return config
})
}
複製程式碼
配置項支援
目前只支援以下配置項
// config.js
module.exports = {
// 標準的webpack4的配置,可以覆蓋預設配置
webpack: {},
// 預設的啟動埠是8018,這裡可以切換
port: 8017,
// 預設設計圖寬度是750,這裡可以修改
viewportWidth: 750,
viewportHeight: 1334,
// 生產環境sourcemap使用'source-map'固定不變,開發環境可以通過devtool去設定
devtool: 'inline-source-map',
// webpack-dev-server代理設定
proxy: {},
// eslint的規則,因為我自己的習慣,將'no-unused-vars'設成了1,這個配置項可以修改預設的
rules: {},
// postcss的外掛,如果自行定製,本地也需安裝一下相應node模組
postcssPlugin: {},
// .eslintrc的配置項,可以覆蓋
eslintConfig: {},
// babel外掛, 預設已經有transform-runtime與transform-decorators-legacy,請不要重複新增
babelPlugins: [],
// babel preset,預設已經有env與stage-2,請不要重複新增
babelPresets: []
}
複製程式碼
cli支援
到這裡就差不多了,接下來需要將使用webpack搭建的工作流整合成cli,這樣做的好處一是可以通過命令列去開發以及構建,同時,可以釋出npm社群後,只需一次安裝即可,即可多次使用,因為qd-cli
內內建vue,vuex,vue-router,axios,jsonp,ramda,jquery
等模組,無需二次安裝,大大減少了專案體積,簡要說明整合成cli是怎麼做到以及一些注意點
-
使用commander搭建cli,可以直接看qd-cli原始碼,主要程式碼在
bin
以及lib/command
目錄下,也可以參考基於node.js的腳手架工具開發經歷 -
webpack的配置項
resolve.modules
代表當require
一個檔案,從這些目錄去檢索,qd-cli
的配置項如下
const cwd = p => path.resolve(__dirname, p)
const ownDir = p => path.join(__dirname, p)
module.exports = {
resolve: {
modules: [cwd(), cwd('node_modules'), ownDir('node_modules'), cwd('../web_modules')]
}
複製程式碼
比如: require('jquery')
在當前專案目錄找不到的話,會前往當前目錄下的node_modules
,還沒找到的話去前往腳手架目錄下的node_modules
, 以及上一層目錄下的web_modules
(專案集支援), 由於腳手架內安裝了jquery
,專案本身就不需要再安裝了,直接依賴即可
- webpack的配置項
resolveLoader
選項,配置如下:
resolveLoader: {
modules: [cwd('node_modules'), ownDir('node_modules')]
},
複製程式碼
主要是webpack
會報錯,說是找不到對應的loader
,這裡要在查詢loader
的路徑列表里加上腳手架目錄下的node_modules
- 腳手架的package.json中需要帶有
bin
欄位
指定qd
命令對應的可執行檔案的位置
"bin": {
"qd": "./bin/cli.js" // 指示cli的執行檔案
}
複製程式碼
./bin/cli.js
最上面一行
#!/usr/bin/env node
複製程式碼
指示用什麼程式去啟動指令碼,我們用的是node
編寫種子檔案
釋出到npm社群
參考如何釋出一個自定義Node.js模組到NPM(詳細步驟,附Git使用方法)
由於qd-cli
的名字npm
社群不給註冊(已經有相似名字的倉庫了),我換成了qd-clis
?
qd-cli安裝與使用
安裝
npm i qd-clis -g
or
yarn global add qd-clis
複製程式碼
window平臺請使用管理員許可權安裝,mac平臺請在命令前面加上sudo
如果你不想全域性安裝的話,拉到本地隨意的目錄並檢視原始碼的話,可以:(同樣要以管理員身份)
git clone git@github.com:nwa2018/qd-cli.git
cd qd-cli
npm i / yarn
npm link
複製程式碼
使用
安裝完畢後,在命令輸入qd
即可看到所有命令簡介,如下圖
如上圖,qd-cli
具備最基礎的生成種子專案,開發與構建三大功能
特性
- qd-cli內建了vue,vuex,vue-router,axios,jsonp,ramda,jquery,無需二次安裝
- 支援es6語法,支援async,await, 支援裝飾器
- eslint採用standard規範
- 支援pug語法,stylus, scss
- 生產環境支援圖片自動壓縮
- 支援單頁應用,多頁應用,支援專案集結構
- 支援少量的配置項
- 支援mock服務
- 生產環境支援壓縮,程式碼分割,懶載入,打雜湊戳等 ...
踩過的一些坑
結合vue-loader,mini-css-extract-plugin外掛無法抽取出css,css被莫名刪掉
webpack guide
的tree-shaking章節建議在package.json
加上
"sideEffects": [
"*.css"
]
複製程式碼
以避免css
檔案被莫名地刪掉,實際上結合了vue-loader
便會被刪掉,解決方案是去掉該選項即可
window平臺下無法啟動webpack
與webpack-dev-server
命令
我是使用shelljs
去啟動打包與開啟伺服器的動作的,程式碼如下
// build.js...
shell.exec(`${ownDir('node_modules/webpack/bin/webpack.js')} --config ${ownDir('lib/webpack/webpack.prod.js')} --progress --report`)
// dev.js...
shell.exec(`${ownDir('node_modules/webpack-dev-server/bin/webpack-dev-server.js')} --config ${ownDir('webpack/webpack.dev.js')} --color`)
複製程式碼
mac平臺下沒問題,window平臺下直接在我的sublime開啟了webpack.dev.js
與webpack.prod.js
- -!,猜測是window平臺下系統不知道該以何種程式去啟動檔案,改成如下即可,加上node
// build.js...
shell.exec(`node ${ownDir('node_modules/webpack/bin/webpack.js')} --config ${ownDir('lib/webpack/webpack.prod.js')} --progress --report`)
// dev.js...
shell.exec(`node ${ownDir('node_modules/webpack-dev-server/bin/webpack-dev-server.js')} --config ${ownDir('webpack/webpack.dev.js')} --color`)
複製程式碼
eslint無法正確解析import()與async await
參考Parse error with import() #7764 與'Parsing error: Unexpected token function' using async/await + ecmaVersion 2017 #8366
一開始報錯我以為是babel的問題,花了很多時間去定位- -!在.eslintrc
中加上如下配置與安裝babel-eslint
即可
parserOptions: {
parser: 'babel-eslint',
ecmaVersion: 2017,
sourceType: 'module'
}
複製程式碼
babel-core沒辦法找到.babelrc
在.babelrc
里加上如下配置,我改成了babel.js
,並跟postcss,eslint的配置一起丟到webpack/config/
目錄下,實際上babel.js
就是我們平時編寫的.babelrc
{
// 傳進去babel配置路徑
filename: ownDir('lib/webpack/config/babel.js'),
}
複製程式碼
參考連結
- Vip Mobile Cli 簡明教程
- vue-cli
- webpack-guide
- 如何釋出一個自定義Node.js模組到NPM(詳細步驟,附Git使用方法)
- 基於node.js的腳手架工具開發經歷
- 如何在Vue專案中使用vw實現移動端適配
github地址,這麼長的文章都看完了,走過路過的帥哥美女,點個讚唄??
本文完。