從零開始搭建腳手架

組內已經有了很是完善以及流暢的開發,發佈流程,平時只須要默默地搬屬於本身的那塊磚就行了,可是每當社區出了新的技術,想嘗試的時候老是欠缺一個「起手式」,能夠快速將新的技術給集成到本身的腳手架,或者說工做流中,基於這個目的,想到就開始作了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()
  ]
}
複製代碼
  • 提供sourcemap

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()
  ]
}
複製代碼

更多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地址,這麼長的文章都看完了,走過路過的帥哥美女,點個讚唄😂😂

本文完。

相關文章
相關標籤/搜索