從零開始搭建腳手架

nw2018發表於2018-05-12

組內已經有了非常完善以及流暢的開發,釋出流程,平時只需要默默地搬屬於自己的那塊磚就好了,但是每當社群出了新的技術,想嘗試的時候總是欠缺一個“起手式”,可以快速將新的技術給整合到自己的腳手架,或者說工作流中,基於這個目的,想到就開始做了

腳手架對團隊的好處不言而喻,可以通過命令列的方式去快速生成種子檔案,開發以及輸出構建後的程式碼,平時我們只需要開發,而不用跟複雜的編譯過程,搭建服務等流程打交道,另外,還可以將我們需要的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()
  ]
}
複製程式碼

更多sourcemap選項

生產環境配一個最簡單的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本身就已經快很多了,這裡使用happypackhappypack啟動多個程式加速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是怎麼做到以及一些注意點

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

編寫種子檔案

qd-vue-seed

釋出到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 guidetree-shaking章節建議在package.json加上

  "sideEffects": [
    "*.css"
  ]
複製程式碼

以避免css檔案被莫名地刪掉,實際上結合了vue-loader便會被刪掉,解決方案是去掉該選項即可

window平臺下無法啟動webpackwebpack-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.jswebpack.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'),
}
複製程式碼

參考連結


github地址,這麼長的文章都看完了,走過路過的帥哥美女,點個讚唄??

本文完。

相關文章