組內已經有了很是完善以及流暢的開發,發佈流程,平時只須要默默地搬屬於本身的那塊磚就行了,可是每當社區出了新的技術,想嘗試的時候老是欠缺一個「起手式」,能夠快速將新的技術給集成到本身的腳手架,或者說工做流中,基於這個目的,想到就開始作了javascript
腳手架對團隊的好處不言而喻,能夠經過命令行的方式去快速生成種子文件,開發以及輸出構建後的代碼,平時咱們只須要開發,而不用跟複雜的編譯過程,搭建服務等流程打交道,另外,還能夠將咱們須要的node
模塊安裝到腳手架內,之後咱們只負責開發而不須要安裝龐大的node_module
了,保持目錄的乾淨,甚至腳手架還能夠跟後續的持續集成相結合,提供更強大的功能css
從零開始搭建腳手架須要必定的前端工程化知識,推薦看webpack指引,裏面涉及了大量前端工程化須要作的事情,事實上我也是從這裏一步一步地往上搭上去的,並最終開發完腳手架qd-cli(音譯:前端-cli,語文很差- -!),開發腳手架本質上仍是寫webpack
,用webpack
搭建工做流,並最終可使用commander將其封裝成命令行工具,這篇文章對commander
介紹得很詳細了,再也不重複:基於node.js的腳手架工具開發經歷html
本文從如下三個方面作介紹,搭建:如何一步步開發qd-cli(包含了我對前端工程化的瞭解)
,qd-cli的安裝,使用,特性
,搭建過程當中遇到的一些坑
前端
先從簡單地作起,再慢慢地往上堆砌,所以,目前考慮的是只支持移動端項目
,以及vue技術棧
vue
技術方案:工做流的編寫毫無懸念地選擇了webpack,現下最熱門的前端打包工具,webpack首要解決了前端模塊化的難題,開箱即用,原生支持es module
,這裏選擇最新的webpack4
,另外一方面,將工做流集成成cli
使用commanderjava
主要考慮如下三個方面:node
在開發環境須要有服務器去啓動並自動刷新咱們的應用,有時甚至指望能夠設置代理,便於先後端聯調,可使用webpack-dev-server,配置很簡單react
// webpack.config.js
module.exports = {
// ...
+ devServer: {
+ ...
+ contentBase: cwd('dist'),
+ proxy: { ... }
+ }
}
複製代碼
在更改代碼後無需手動刷新瀏覽器便可預覽效果,快速便捷,即便js的熱重載有點坑,有時須要手動去刷新,但整體仍是利大於弊的jquery
const webpack = require('webpack');
module.exports = {
devServer: {
...
+ hot: true,
contentBase: cwd('dist'),
proxy: { ... }
},
plugins: [
+ new webpack.NamedModulesPlugin(),
+ new webpack.HotModuleReplacementPlugin()
]
}
複製代碼
webpack打包後的代碼報錯後不利於咱們去定位錯誤位置,soucemap
能夠幫咱們準肯定位到源碼的出錯位置webpack
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
}
}
}
}
}
複製代碼
搖樹利用了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的緩存,使用官方推薦的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
相比之前,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比較熟悉,這裏選用了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服務器生成假數據編寫顯示邏輯,這裏使用本身擼的輪子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: []
}
複製代碼
到這裏就差很少了,接下來須要將使用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
,項目自己就不須要再安裝了,直接依賴便可
resolveLoader
選項,配置以下:resolveLoader: {
modules: [cwd('node_modules'), ownDir('node_modules')]
},
複製代碼
主要是webpack
會報錯,說是找不到對應的loader
,這裏要在查找loader
的路徑列表里加上腳手架目錄下的node_modules
bin
字段指定qd
命令對應的可執行文件的位置
"bin": {
"qd": "./bin/cli.js" // 指示cli的執行文件
}
複製代碼
./bin/cli.js
最上面一行#!/usr/bin/env node
複製代碼
指示用什麼程序去啓動腳本,咱們用的是node
參考如何發佈一個自定義Node.js模塊到NPM(詳細步驟,附Git使用方法)
因爲qd-cli
的名字npm
社區不給註冊(已經有類似名字的倉庫了),我換成了qd-clis
😂
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
具有最基礎的生成種子項目,開發與構建三大功能
webpack guide
的tree-shaking章節建議在package.json
加上
"sideEffects": [
"*.css"
]
複製代碼
以免css
文件被莫名地刪掉,實際上結合了vue-loader
便會被刪掉,解決方案是去掉該選項便可
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`)
複製代碼
參考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'
}
複製代碼
在.babelrc
里加上以下配置,我改爲了babel.js
,並跟postcss,eslint的配置一塊兒丟到webpack/config/
目錄下,實際上babel.js
就是咱們平時編寫的.babelrc
{
// 傳進去babel配置路徑
filename: ownDir('lib/webpack/config/babel.js'),
}
複製代碼
github地址,這麼長的文章都看完了,走過路過的帥哥美女,點個讚唄😂😂
本文完。