學習一個Vue模板項目

最開始學習Vue的時候,不建議直接使用模板,而應該本身從頭寫起。模板都是人寫的,要堅信「人能我能」。只有本身親自實踐,才能促進本身主動思考,才能對模板、框架有深入的理解。css

在Github上看到一個Vue的項目模板,裏面包含許多新的知識。仔細研究,所獲甚豐。html

新庫

  • ora:用於美觀地打印正在執行的步驟,是一個控制檯打印小程序
const spinner = ora('building for production...')
spinner.start()
doSomeThing(args,()=>{
  spinner.stop()
})
  • chalk:彩色粉筆,是一個控制檯打印小程序
  • rimraf:rm命令的js實現
  • node-notifier:兼容多個操做系統的通知小程序,運行下列代碼會在底部彈出通知
const nodeNotifier=require("node-notifier")
nodeNotifier.notify("hello world")

notify能夠接受一個JSON。前端

notifier.notify({
  title: "title",
  message: "message",
  subtitle:  'subtitle',
  icon: path.join(__dirname, 'logo.png')
})
  • jest:JavaScript中最流行的測試框架
  • webpack-merge:用於合併配置,後面的配置優先級高於前面的配置。
  • semver:sematic version,語義化的版本工具,用於獲取庫的版本號,比較庫的版本號大小等
  • shelljs:使用Js調用控制檯命令
  • portfinder:尋找未被佔用的端口

使用Node編寫工具

NodeJs的兩大做用:vue

  • 改變了前端,使前端走上了工具化道路
  • 改變了後端,NodeJS使JS能夠用來編寫後端代碼

使用Node編寫工具經常使用的庫上面已經提到了node

  • 人性化的展現:ora、chalk、node-notifier
  • 操做系統命令:rimraf、shelljs

配置文件

  • .babelrc:用於配置babel
  • .editorconfig:用於配置編輯器對文本文件的處理方式,大部分IDE、編輯器都有相應的插件
  • .eslintignore .eslintrc.js
  • .gitkeep:git是不容許提交一個空的目錄到版本庫上的,能夠在空的文件夾裏面創建一個.gitkeep文件,而後提交去便可。其實在git中 .gitkeep 就是一個佔位符。能夠用其餘 好比 .nofile等文件做爲佔位符。
  • package.json
{
  ...
  "scripts":{
      "dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js",
      "start": "npm run dev",
      "unit": "jest --config test/unit/jest.conf.js --coverage",
      "test": "npm run unit",
      "lint": "eslint --ext .js,.vue src test/unit",
      "build": "node build/build.js"
  }
  ...
}

從source-map提及

source-map解決了SPA程序難以定位錯誤代碼行數的問題。
若是是我第一個採用SPA方式開發程序,我極可能會由於難以定位錯誤代碼行數而放棄SPA技術方案。但如今人們已經經過source-map的方式解決了這個問題。
Vue框架的工具鏈包括Chrome插件、Vue-Loader、IDE的Vue插件、Vue-Cli等,這些工具鏈造就了Vue的易用性。
任何好庫必然都有一個優秀的工具鏈,工具鏈可以促進庫的推廣。
適當地學習經常使用系統的插件開發是製做工具的必備常識,要掌握VSCode、Chrome、Intellij Idea等系統的插件開發。webpack

使用NodeJS檢查版本

check-version.js文件用於檢測系統環境,即檢測node和npm的版本是否知足要求。
一個好的應用應該主動檢測環境,不然一旦出錯,就會無從下手。git

check-version.jsgithub

const chalk = require("chalk")
const semver = require("semver")
const packageConfig = require("../package.json")
const shell = require("shelljs")

function exec(cmd) {
  return require("child_process")
    .execSync(cmd)
    .toString()
    .trim()
}

const versionRequirements = [
  {
    name: "node",
    currentVersion: semver.clean(process.version),
    versionRequirement: packageConfig.engines.node
  }
]

if (shell.which("npm")) {
  versionRequirements.push({
    name: "npm",
    currentVersion: exec("npm --version"),
    versionRequirement: packageConfig.engines.npm
  })
}

module.exports = function() {
  const warnings = []

  for (let i = 0; i < versionRequirements.length; i++) {
    const mod = versionRequirements[i]

    if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) {
      warnings.push(mod.name + ": " + chalk.red(mod.currentVersion) + " should be " + chalk.green(mod.versionRequirement))
    }
  }

  if (warnings.length) {
    console.log(chalk.yellow("To use this template, you must update following to modules:"))

    for (let i = 0; i < warnings.length; i++) {
      const warning = warnings[i]
      console.log("  " + warning)
    }

    console.log()
    process.exit(1)
  }
}

三種模式

mode有三種模式:web

  • 開發環境:development
  • 測試環境:test
  • 生產環境:production

測試環境和開發環境的配置幾乎是徹底同樣的,生產環境在打包方面進行了許多優化,使得打包文件更小。shell

執行構建過程,打包生產環境工具

當執行npm run build時,mode爲production。此命令執行如下步驟:

  • 調用那個check-versions.js,檢查node和npm的版本
  • 刪除目標目錄(默認爲dist)中的所有文件,防止舊文件影響新文件
  • 調用webpack庫根據webpack.prod.conf.js進行打包

build.js

require('./check-versions')()

process.env.NODE_ENV = 'production'

const ora = require('ora')
const rm = require('rimraf')
const path = require('path')
const chalk = require('chalk')
const webpack = require('webpack')
const config = require('../config')
const webpackConfig = require('./webpack.prod.conf')

const spinner = ora('building for production...')
spinner.start()

rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => {
  if (err) throw err
  webpack(webpackConfig, (err, stats) => {
    spinner.stop()
    if (err) throw err
    process.stdout.write(stats.toString({
      colors: true,
      modules: false,
      children: false, // If you are using ts-loader, setting this to true will make TypeScript errors show up during build.
      chunks: false,
      chunkModules: false
    }) + '\n\n')

    if (stats.hasErrors()) {
      console.log(chalk.red('  Build failed with errors.\n'))
      process.exit(1)
    }

    console.log(chalk.cyan('  Build complete.\n'))
    console.log(chalk.yellow(
      '  Tip: built files are meant to be served over an HTTP server.\n' +
      '  Opening index.html over file:// won\'t work.\n'
    ))
  })
})

在這段代碼中,須要掌握在JS中調用webpack,webpack第一個參數爲配置對象,第二個參數爲回調函數,回調函數包括兩個參數(err,stats)。

require的用法

在NodeJS中引入其它模塊時,使用const xxx=require("moduleName")的寫法。注意若是引入的模塊是js文件,不要帶文件後綴名.js。

  • 每一個文件中的exports默認是一個字典,能夠直接使用exports.xxx=...的方式往上面掛東西,可是不能exports=...來改變exports的值,由於改變以後module.exports再也不等於exports
console.log(global.exports)//undifined
console.log(exports)//{}
console.log(module.exports)//{}
exports = "haha"
console.log(exports)//"haha"
console.log(module.exports)//{}
  • require能夠導入JSON文件,如
const haha=require("./haha.json")
//等價於
const haha=JSON.parse(fs.readFileSync(path.join(__dirname,'haha.json')).toString("utf8"))

自動生成CSS的webpack配置

寫CSS有不少選擇:css、less、sass、scss、stylus、postcss。若是爲這些語言都配置loader,webpack的配置會顯得很是冗長。webpack配置本質就是JSON,咱們可使用JavaScript來生成JSON。

const cssLoaders = function (options) {
  options = options || {}

  const cssLoader = {
    loader: 'css-loader',
    options: {
      sourceMap: options.sourceMap
    }
  }

  const postcssLoader = {
    loader: 'postcss-loader',
    options: {
      sourceMap: options.sourceMap
    }
  }

  // generate loader string to be used with extract text plugin
  function generateLoaders (loader, loaderOptions) {
    const loaders = options.usePostCSS ? [cssLoader, postcssLoader] : [cssLoader]

    if (loader) {
      loaders.push({
        loader: loader + '-loader',
        options: Object.assign({}, loaderOptions, {
          sourceMap: options.sourceMap
        })
      })
    }

    // Extract CSS when that option is specified
    // (which is the case during production build)
    if (options.extract) {
      return ExtractTextPlugin.extract({
        use: loaders,
        fallback: 'vue-style-loader'
      })
    } else {
      return ['vue-style-loader'].concat(loaders)
    }
  }

  // https://vue-loader.vuejs.org/en/configurations/extract-css.html
  return {
    css: generateLoaders(),
    postcss: generateLoaders(),
    less: generateLoaders('less'),
    sass: generateLoaders('sass', { indentedSyntax: true }),
    scss: generateLoaders('sass'),
    stylus: generateLoaders('stylus')
  }
}

// Generate loaders for standalone style files (outside of .vue)
exports.styleLoaders = function (options) {
  const output = []
  const loaders = cssLoaders(options)

  for (const extension in loaders) {
    const loader = loaders[extension]
    output.push({
      test: new RegExp('\\.' + extension + '$'),
      use: loader
    })
  }

  return output
}

在配置VueLoader時,不須要指明less的loader。VueLoader會根據webpack配置的規則尋找loader,就好像Vue中的CSS部分就像單個文件同樣。

webpack基本配置

node選項

打包時,不必把一些包打包進去。

node: {
  // prevent webpack from injecting useless setImmediate polyfill because Vue
  // source contains it (although only uses it if it's native).
  setImmediate: false,
  // prevent webpack from injecting mocks to Node native modules
  // that does not make sense for the client
  dgram: "empty",
  fs: "empty",
  net: "empty",
  tls: "empty",
  child_process: "empty"
}

context選項

設置webpack尋找文件的根目錄,這樣就不用每一個路徑都是用path.join(__dirname,xxxx)這種複雜寫法了。context選項的值必須是絕對路徑,entry 和 module.rules.loader選項相對於此路徑進行解析。不建議使用context,由於它語義不夠明確,不如手動路徑拼接來得清晰。

context: path.resolve(__dirname, "../"),

resolve

文檔

  • alias用於控制引入的包名所對應的真實包,vue$表示精確匹配vue。
  • descriptionFiles默認爲package.json,表示每一個包的描述文件
  • enforceExtension表示require('xxxx')中是否強制要求帶後綴名,默認爲false
  • resolve.extensions表示處理導入時能夠忽略後綴名的文件,默認爲['.js']。在使用ElementUI時,必須把.vue加上,不然ElementUI中的某個庫會報錯。
resolve: {
  extensions: [".js", ".vue", ".json"],
  alias: {
    vue$: "vue/dist/vue.esm.js",
    "@": path.join(__dirname, "../src")
  }
}

module.rules

rules配置主要包括vue配置vue-loader,js配置babel-loader,各類類型的資源文件配置url-loader。這是一份基本配置,更多配置須要在開發模式和生產模式中補充。

module: {
  rules: [
    {
      test: /\.vue$/,
      loader: "vue-loader",
      options: vueLoaderConfig
    },
    {
      test: /\.js$/,
      loader: "babel-loader",
      include: [path.join(__dirname,"src"), path.join(__dirname,"test"), path.join(__dirname,"node_modules/webpack-dev-server/client")]
    },
    {
      test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
      loader: "url-loader",
      options: {
        limit: 10000,
        name: utils.assetsPath("img/[name].[hash:7].[ext]")
      }
    },
    {
      test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
      loader: "url-loader",
      options: {
        limit: 10000,
        name: utils.assetsPath("media/[name].[hash:7].[ext]")
      }
    },
    {
      test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
      loader: "url-loader",
      options: {
        limit: 10000,
        name: utils.assetsPath("fonts/[name].[hash:7].[ext]")
      }
    }
  ]
}

開發模式下的webpack

動態端口號

開發模式的webpack配置文件名爲webpack.dev.conf.js。webpack編譯時不必定接受一個JSON對象,也能夠接受一個Promise對象。在開發模式下module.exports就是一個Promise對象。使用Promise的緣由是,開發服務器的端口號須要使用portfinder動態肯定。

下面代碼中的devWebpackConfig就是開發模式下的基本配置。
webpack.dev.conf.js

const portfinder = require('portfinder')
module.exports = new Promise((resolve, reject) => {
  portfinder.basePort = process.env.PORT || config.dev.port
  portfinder.getPort((err, port) => {
    if (err) {
      reject(err)
    } else {
      // publish the new Port, necessary for e2e tests
      process.env.PORT = port
      // add port to devServer config
      devWebpackConfig.devServer.port = port

      // Add FriendlyErrorsPlugin
      devWebpackConfig.plugins.push(new FriendlyErrorsPlugin({
        compilationSuccessInfo: {
          messages: [`Your application is running here: http://${devWebpackConfig.devServer.host}:${port}`],
        },
        onErrors: config.dev.notifyOnErrors
        ? utils.createNotifierCallback()
        : undefined
      }))

      resolve(devWebpackConfig)
    }
  })
})

開發模式下的插件

webpack提供了豐富的插件,每一個插件都有不一樣的配置,瞭解經常使用插件是頗有必要的,可是許多插件又是無關緊要的。

plugins: [
  new webpack.DefinePlugin({
    //config/dev.env中定義了一些常量
    'process.env': require('../config/dev.env')
  }),
  //模塊熱替換插件
  new webpack.HotModuleReplacementPlugin(),
  new webpack.NamedModulesPlugin(), // NMP shows correct file names in console on update.
  new webpack.NoEmitOnErrorsPlugin(),
  // https://github.com/ampedandwired/html-webpack-plugin
  new HtmlWebpackPlugin({
    filename: 'index.html',
    template: 'index.html',
    inject: true
  }),
  // copy custom static assets
  new CopyWebpackPlugin([
    {
      from: path.resolve(__dirname, '../static'),
      to: config.dev.assetsSubDirectory,
      ignore: ['.*']
    }
  ])
]

DifinePlugin咱們徹底能夠手動實現,過程也不復雜,我認爲本身實現可讀性、靈活性更好,不必定義成一個插件。
HotModuleReplacementPlugin、NamedModulesPlugin、NoEmitOnErrorsPlugin這些插件用來在瀏覽器端或者在控制檯下打印更規整信息,便於調試。
HtmlWebpackPlugin這個插件更是沒有必要,它提供的功能太簡單了。
CopyWebpackPlugin這個插件很是有用,當須要把靜態文件原封不動地複製到dist目錄時,使用這個插件。

若是index.html在dist目錄中,那麼咱們就須要使用CopyWebpackPlugin把靜態文件都放到dist目錄中。開發時devServer須要配置contentBase='dist',從而指明index.html的位置。

開發模式下的配置

開發模式下的配置在基本配置的基礎上略加修改。

const utils = require("./utils")
const config = require("../config")
const merge = require("webpack-merge")
const path = require("path")
const baseWebpackConfig = require("./webpack.base.conf")
const HOST = process.env.HOST
const PORT = process.env.PORT && Number(process.env.PORT)

const devWebpackConfig = merge(baseWebpackConfig, {
  module: {
    rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, usePostCSS: true })
  },
  // these devServer options should be customized in /config/index.js
  devServer: {
    clientLogLevel: "warning",
    historyApiFallback: {
      rewrites: [{ from: /.*/, to: path.posix.join(config.dev.assetsPublicPath, "index.html") }]
    },
    hot: true,
    contentBase: false, // since we use CopyWebpackPlugin.
    compress: true,
    host: HOST || config.dev.host,
    port: PORT || config.dev.port,
    open: config.dev.autoOpenBrowser,
    overlay: config.dev.errorOverlay ? { warnings: false, errors: true } : false,
    publicPath: config.dev.assetsPublicPath,
    proxy: {
      '/api': {
        target: 'http://localhost:8081',
        // secure: false,      // 若是是https接口,須要配置這個參數
        changeOrigin: true, // 若是接口跨域,須要進行這個參數配置
        pathRewrite: {
          '^/api': ''
        }
      }
    }
  }
})

生產模式下的webpack配置

把CSS單獨抽離出去

module: {
  rules: utils.styleLoaders({
    sourceMap: false,
    extract: true,
    usePostCSS: true
  })
}

在plugin中

// extract css into its own file
new ExtractTextPlugin({
  filename: utils.assetsPath('css/[name].[contenthash].css'),
  // Setting the following option to `false` will not extract CSS from codesplit chunks.
  // Their CSS will instead be inserted dynamically with style-loader when the codesplit chunk has been loaded by webpack.
  // It's currently set to `true` because we are seeing that sourcemaps are included in the codesplit bundle as well when it's `false`,
  // increasing file size: https://github.com/vuejs-templates/webpack/issues/1110
  allChunks: true,
})

優化CSS,去掉不一樣組件之間的重複CSS

new OptimizeCSSPlugin({
  cssProcessorOptions: config.build.productionSourceMap
    ? { safe: true, map: { inline: false } }
    : { safe: true }
})

使用多個chunk

輸出的文件名須要帶上chunkhash

output: {
  path: config.build.assetsRoot,
  filename: utils.assetsPath('js/[name].[chunkhash].js'),
  chunkFilename: utils.assetsPath('js/[id].[chunkhash].js')
}

須要使用CommonsChunkWebpackPlugin

// split vendor js into its own file
new webpack.optimize.CommonsChunkPlugin({
  name: 'vendor',
  minChunks (module) {
    // any required modules inside node_modules are extracted to vendor
    return (
      module.resource &&
      /\.js$/.test(module.resource) &&
      module.resource.indexOf(
        path.join(__dirname, '../node_modules')
      ) === 0
    )
  }
}),
// extract webpack runtime and module manifest to its own file in order to
// prevent vendor hash from being updated whenever app bundle is updated
new webpack.optimize.CommonsChunkPlugin({
  name: 'manifest',
  minChunks: Infinity
}),
// This instance extracts shared chunks from code splitted chunks and bundles them
// in a separate chunk, similar to the vendor chunk
// see: https://webpack.js.org/plugins/commons-chunk-plugin/#extra-async-commons-chunk
new webpack.optimize.CommonsChunkPlugin({
  name: 'app',
  async: 'vendor-async',
  children: true,
  minChunks: 3
})

使用HtmlWebpackPlugin

HtmlWebpackPlugin並不是一無可取,對於多個entry的webpack項目,它可以爲每一個入口生成index.html

new HtmlWebpackPlugin({
  filename: process.env.NODE_ENV === 'testing'
    ? 'index.html'
    : config.build.index,
  template: 'index.html',
  inject: true,
  minify: {
    removeComments: true,
    collapseWhitespace: true,
    removeAttributeQuotes: true
    // more options:
    // https://github.com/kangax/html-minifier#options-quick-reference
  },
  // necessary to consistently work with multiple chunks via CommonsChunkPlugin
  chunksSortMode: 'dependency'
})

CompressionWebpackPlugin

const CompressionWebpackPlugin = require('compression-webpack-plugin')

webpackConfig.plugins.push(
  new CompressionWebpackPlugin({
    asset: '[path].gz[query]',
    algorithm: 'gzip',
    test: new RegExp(
      '\\.(' + ['js', 'css'].join('|') +
      ')$'
    ),
    threshold: 10240,
    minRatio: 0.8
  })
)

Vue項目壓縮的幾種方法

優化前端項目有以下幾個出發點:

  • 減少體積:上策
  • 減小請求次數:屢次請求小文件是很浪費資源的
  • 懶加載:不要一次性加載太多東西
  • 使用CDN

具體措施:

  • 使用bootcdn上的Vue、ElementUI,不要把大庫打包進最終包裏面
  • 使用commons-chunk分塊加載,不要一次性加載所有組件
  • 使用ExtractTextPlugin把CSS分離出去,單獨加載CSS
  • 啓用GZIP壓縮
  • 減小網絡請求次數,如:把較小的資源文件使用base64編碼
  • 去掉source-map
  • 使用正確的Vue版本,使用壓縮版的Vue。若是使用SPA,不須要使用模板編譯模塊。
相關文章
相關標籤/搜索