組內已經有了很是完善以及流暢的開發,發佈流程,平時只須要默默地搬屬於本身的那塊磚就行了,可是每當社區出了新的技術,想嘗試的時候老是欠缺一個「起手式」,能夠快速將新的技術給集成到本身的腳手架,或者說工做流中,基於這個目的,想到就開始作了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地址,這麼長的文章都看完了,走過路過的帥哥美女,點個讚唄😂😂
本文完。