Vue 脚手架项目分析

2018-08-30 10:53:06 浏览数 (1)

  • vue cli创建后的目录

vue cli创建后的目录.png

代码语言:javascript复制
build:webpack的一些配置文件以及服务启动文件
config:多为build中所依赖的文件
src: 页面以及逻辑文件夹
static: 字体以及公共样式文件夹
.babelrc: es6编译文件配置,将es6编译为es5
.editorconfig: 编写风格配置文件,比如缩进文件格式等等
.eslintignore: 忽略检测一些文件格式,比如我们默认忽略检测build以及config中的js
.eslintrc.js: 代码规范化配置文件
.gitignore: 忽略上传一些文件或配置
.postcsssrc.js: 用js来处理css
index.html: 主文件入口
package.json: npm依赖以及开发生产环境所以来的模块包
README.md: 解释说明一下本项目是做什么的

babel 配置


配置文件就是.babelrc,使用这个来进行配置

代码语言:javascript复制
{
  "presets": [
    ["env", {
      "modules": false,
      "targets": {
        "browsers": ["> 1%", "last 2 versions", "not ie <= 8"]
      }
    }],
    "stage-2"
  ],
  "plugins": ["transform-vue-jsx", "transform-runtime"]
}

presets字段设定转码规则,官方提供以下的规则集,你可以根据需要安装。

代码语言:javascript复制
npm install --save-dev babel-preset-stage-2
babel-preset-env

babel-preset-env是一个新的预设,可以让你指定一个环境并自动使能需要的插件。 支持拥有超过1%市场份额的浏览器

plugin

transform-vue-jsx 在Vue中使用jsx的插件。

来一个简单的例子:
  • 原来:
代码语言:javascript复制
 render (createElement) {
    return createElement(
      'span',
      {
        class: { 'my-class': true },
        style: { cursor: 'pointer' },
        on: {
          click: this.hello
        }
      },
      [ this.msg ]
    );
  },
  • 使用jsx
代码语言:javascript复制
new Vue({
  el: '#app',
  data: {
    msg: 'Click to see the message.'
  },
  methods: {
    hello () {
      alert('This is the message.')
    }
  },
  render: function render(h) {
    return (
      <span
        class={{ 'my-class': true }}
        style={{ cursor: 'pointer' }}
        on-click={ this.hello }
      >
        { this.msg }
      </span>
    )
  }
});
配合的webpack配置
代码语言:javascript复制
loaders: [
  { test: /.js$/, loader: 'babel', exclude: /node_modules/ }
]
run-time

babel-polyfill vs babel-runtime 那什么时候用 babel-polyfill 什么时候用 babel-runtime 呢?如果你不介意污染全局变量(如上面提到的业务代码),放心大胆地用 babel-polyfill ;而如果你在写模块,为了避免污染使用者的环境,没的选,只能用 babel-runtime babel-plugin-transform-runtime。

因为 babel-runtime 都是自动帮你引入所需 polyfill,但每个文件引入的都是全量的 polyfill,很容易引起体积暴涨。所以我们可以加上 babel-plugin-transform-runtime,这样 runtime 所用的 polyfill 就会从 transform-runtime 里引入,就避免了体积暴涨的问题。

应该这么说,Babel 中 runtime 只是个 helper 函数库,runtime transform 根据 ast 结果帮你引入所需 helper 函数。所以是只用 runtime 是需要手动,但不要这样用,而是用 runtime transform。

.editorconfig


EditorConfig 是一个编辑器/IDE 偏好设置的标准,在各大主流编辑器/IDE 平台都支持或者拥有相应的插件。它能够帮助你根据项目自动设置编辑器/IDE 的代码偏好。 EditorConfig 的配置文件是 ini 格式的纯文本文件,因此对于版本控制程序来说非常友好。 EditorConfig 插件会自动在项目中寻找名为 .editorconfig 的配置文件,每个文件的样式偏好会自动根据该文件所在文件夹的 .editorconfig 文件向上寻找所有同名文件,直到某个配置的文件种包含了 root=true。最接近该文件的配置文件中的设置优先最高。

代码语言:javascript复制
root = true

[*]
charset = utf-8
# 缩进风格为空格
indent_style = space
# 缩进大小为2
indent_size = 2
# 表示以 Unix 风格的换行符结尾
end_of_line = lf
insert_final_newline = true
#设为 true 表示会除去换行行首的任意空白字符。
trim_trailing_whitespace = true

.gitignore


git忽略的文件

.postcssrc.js


代码语言:javascript复制
module.exports = {
  "plugins": {
    "postcss-import": {},
    "postcss-url": {},
    // to edit target browsers: use "browserslist" field in package.json
    "autoprefixer": {}
  }
}
css文件的统一转换和处理。

通过js转换样式的工具 由vue-loader 处理的 CSS 输出,都是通过 PostCSS 进行作用域重写,你还可以为 PostCSS 添加自定义插件,例如 autoprefixer 或者 CSSNext。 vue-loader 支持通过 postcss-loader 自动加载同一个配置文件:

  • postcss.config.js
  • .postcssrc
  • package.json 中的 postcss

使用配置文件允许你在由 postcss-loader 处理的普通CSS文件和 *.vue 文件中的 CSS 之间共享相同的配置,这是推荐的做法。

webpack配置


Webpack是当下最热门的前端资源模块化管理和打包工。它不像requirejs那样通过rjs进行打包出来的是一个很臃肿的包,它可以将很多松散的模块按照以来以及一定的规则打包成符合生产环境的前端资源。同时,它还具有按需加载,等到实际需要的时候进行异步加载。通过loader的转换,在项目中任何形式的资源都可以被理解为模块。比如图片、css、less、sass等等。

webpack.base.conf.js

代码语言:javascript复制
'use strict'
const path = require('path')
const utils = require('./utils')
const config = require('../config')
const vueLoaderConfig = require('./vue-loader.conf')

function resolve (dir) {
  return path.join(__dirname, '..', dir)
}

module.exports = {
  context: path.resolve(__dirname, '../'),
  //必备要素之一,入口
  entry: {
    app: './src/main.js'
  },
//输出文件。打包到哪
  output: {
  //读取utils index.js中的build的assetRoot,就是 'dist'文件夹
    path: config.build.assetsRoot,
// 导出文件的文件名
    filename: '[name].js',
 // 生产模式或开发模式下的html、js等文件内部引用的公共路径
    publicPath: process.env.NODE_ENV === 'production'
      ? config.build.assetsPublicPath
      : config.dev.assetsPublicPath
  },
//文件解析 resolve
  resolve: { 
// 自动解析确定的扩展名,使导入模块时不带扩展名
    extensions: ['.js', '.vue', '.json'],
    alias: {
// 创建import或 require的别名
      'vue$': 'vue/dist/vue.esm.js',
//用@来代替src
      '@': resolve('src'),
    }
  },
//如何处理项目中不同类型的模块
  module: {
    rules: [
      {
        test: /.vue$/,// vue文件后缀
        loader: 'vue-loader',// 使用vue-loader进行处理
        options: vueLoaderConfig// 对vue-loader做的额外的选项配置
      },
      {
        test: /.js$/,  //js 文件后缀
        loader: 'babel-loader',  // 使用babel-loader进行处理
//必须处理包含src、test的文件夹
        include: [resolve('src'), resolve('test'), resolve('node_modules/webpack-dev-server/client')]
      },
      {
 // 图片后缀
        test: /.(png|jpe?g|gif|svg)(?.*)?$/,
 // 使用url-loader处理
        loader: 'url-loader',
//对loader做的额外配置
        options: {
// 对于小于10Kb的以base64进行引用
          limit: 10000,
 // 文件名为name.7位hash值.扩展名
          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]')
        }
      }
    ]
  },
  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'
  }
webpack.dev.conf.js
代码语言:javascript复制
module: {
    // 通过传入一些配置来获取rules配置,此处传入了sourceMap: false,表示不生成sourceMap
    rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap })
  }
styleLoaders/cssLoader

在 util.js文件下 中的styleLoaders的配置如下面所示:

  • styleLoaders
代码语言:javascript复制
exports.styleLoaders = function (options) {
  var output = [];  // 定义返回数组,数组中保存的是针对各类型的样式文件的处理方式

  var loaders = exports.cssLoaders(options) // 调用cssLoaders方法返回各类型的样式对象 (css: loader)
  for (var extension in loaders) {  // 遍历loaders
    var loader = loaders[extension] // 根据遍历获得的key(extension) 来得到value (loader)
    output.push({
      test: new RegExp(‘\.’   extension   ‘$’),  //  处理文件的类型
      use: loader // 用loader来处理,loader来自loaders【extension】
    })
  }
  return output
}

既然上面的代码调用了cssloaders,那么我们就继续看看在util.js中的另外一个方法—cssloaders是怎么实现的吧.

  • cssLoaders
代码语言:javascript复制
exports.cssLoaders = function (options) {
  options = options || {}

  var cssLoader = {
    loader: 'css-loader',
    options: { // options 是 css-loader的选项配置
      minimize: process.env.NODE_ENV === ‘production’, // 生成生产环境下的压缩文件
      sourceMap: options.sourceMap // 根据参数是否要生成sourceMap文件
    }
  }

  // generate loader string to be used with extract text plugin
  function generateLoaders (loader, loaderOptions) {   // 生成loader
    var loaders = [cssLoader]  // 默认的loader是css-loader
    if (loader) { // 如果参数loader还在
      loaders.push({
        loader: loader   '-loader',
        options: Object.assign({}, loaderOptions, {  // 将loaderOptions和sourceMap组成一个对象
          sourceMap: options.sourceMap
        })
      })
    }

    // Extract CSS when that option is specified
    // (which is the case during production build)
    if (options.extract) {  // 如果传入的options存在extract并且为true
      return ExtractTextPlugin.extract({  // ExtractTextPlugin 分离js中引入的css文件
        use: loaders, // 处理loader
        fallback: ‘vue-style-loader’ // 没有被提取分离时使用的loader
      })
    } else {
      return ['vue-style-loader'].concat(loaders)
    }
  }

  // http://vuejs.github.io/vue-loader/en/configurations/extract-css.html
  return { // 返回css类型对应的loader组成的对象  generateLoaders()来生成loader
    css: generateLoaders(),
    postcss: generateLoaders(),
    less: generateLoaders('less'),
    sass: generateLoaders('sass', { indentedSyntax: true }),
    scss: generateLoaders('sass'),
    stylus: generateLoaders('stylus'),
    styl: generateLoaders('stylus')
  }
}
  • plugins
代码语言:javascript复制
plugins: [
//定义全局变量
    new webpack.DefinePlugin({  
      ‘process.env’: config.dev.env // 当前环境为开发环境
    }),
    // https://github.com/glenjamin/webpack-hot-middleware#installation--usage
    new webpack.HotModuleReplacementPlugin(), // 热更新插件
 new webpack.NamedModulesPlugin(), //这个插件的作用是在热加载时直接返回更新文件名,而不是文件的id。
    new webpack.NoEmitOnErrorsPlugin(), // 不触发错误,即编译后运行的包正常运行
    // https://github.com/ampedandwired/html-webpack-plugin 
    new HtmlWebpackPlugin({ // 自动生成html文件,比如编译后文件的引用
      filename: ‘index.html’, // 生成的文件名
      template: ‘index.html’, // 模板
      inject: true
    }),
 // copy custom static assets
//将资源进行复制的插件
    new CopyWebpackPlugin([
      {
        from: path.resolve(__dirname, '../static'),
        to: config.dev.assetsSubDirectory,
        ignore: ['.*']
      }
    ]),
    new FriendlyErrorsPlugin() // 友好的错误提示
  ]

以上就是我们的开发环境配置,下面我们来看一下生产环境的配置:

webpack.prod.conf.js
代码语言:javascript复制
const baseWebpackConfig = require('./webpack.base.conf')
const merge = require('webpack-merge')

const webpackConfig = merge(baseWebpackConfig, {...})

生产环境下的webpack配置,通过merge方法合并webpack.base.conf.js基础配置。module的处理,主要是针对css的处理。同样的此处调用了utils.styleLoaders

代码语言:javascript复制
module: {  
    rules: utils.styleLoaders({    
    sourceMap: config.build.productionSourceMap,     
    extract: true   
 }) 
}
  • 输出文件 output
代码语言:javascript复制
output: {   
    path: config.build.assetsRoot,  //导出文件目录 
    filename: utils.assetsPath('js/[name].[chunkhash].js'),   //导出的文件名 
    chunkFilename: utils.assetsPath('js/[id].[chunkhash].js')   //非入口文件的文件名,而又需要被打包出来的文件命名配置,如按需加载的模块 
}
  • 插件plugins
代码语言:javascript复制
  plugins: [
    // http://vuejs.github.io/vue-loader/en/workflow/production.html
    new webpack.DefinePlugin({
      'process.env': env  // 配置全局环境为生产环境
    }),
    new webpack.optimize.UglifyJsPlugin({ // js压缩插件
      compress: { // 压缩配置
        warnings: false //不显示警告
      },
      sourceMap: true // 生成sourceMap文件
    }),
    // extract css into its own file
    new ExtractTextPlugin({ // 将js中引入css分离插件
      filename: utils.assetsPath('css/[name].[contenthash].css') // 分离出来的css文件名
    }),
 
    new OptimizeCSSPlugin(),    // 压缩提取出的css,并解决ExtractTextPlugin分离出的js重复问题(多个文件引用同一个css文件)
    // generate dist index.html with correct asset hash for caching.
    // you can customize output by editing /index.html
    // see https://github.com/ampedandwired/html-webpack-plugin
    new HtmlWebpackPlugin({  //生成html的插件,引入css文件和js文件
      filename: config.build.index, // 生成的html的文件名
      template: 'index.html', // 依据的模板
      inject: true, // 注入的js文件会被放在Body标签中,当值为'head'的时候,将被放在head标签中
      minify: { // 压缩配置
        removeComments: true, // 删除html中的注释代码
        collapseWhitespace: true, // 删除html中的空白符
        removeAttributeQuotes: true // 删除html元素中属性的引号
        // more options:
        // https://github.com/kangax/html-minifier#options-quick-reference
      },
      // necessary to consistently work with multiple chunks via CommonsChunkPlugin
      chunksSortMode: 'dependency' //按dependency的顺序引入
    }),
    // split vendor js into its own file
    new webpack.optimize.CommonsChunkPlugin({ //分离公共js到vendor中
      name: 'vendor', // 文件名
      minChunks: function (module, count) { // 声明公告的模块来自 node_modules文件夹
        // 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
        )
      }
    }),
//上面虽然已经分离了第三方库,每次修改编译都会改变vendor的hash值,导致浏览器缓存失效。
// 原因是vendor包含了webpack在打包过程中会产生一些运行时代码,运行时代码中实际上保存了打包后的文件名。
// 当修改业务代码时,业务代码的js文件的hash值必然会改变。一旦改变必然会导致vendor变化。vendor变化会导致其hash值变化。
    // 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({  //下面主要是将运行时代码提取到单独的manifest文件中,防止其影响vendor.js
      name: 'manifest',
      chunks: ['vendor']
    }),
    // copy custom static assets
    new CopyWebpackPlugin([  // 复制静态资源,将static文件内的内容复制到指定文件夹
      {
        from: path.resolve(__dirname, '../static'),
        to: config.build.assetsSubDirectory,
        ignore: ['.*']  //忽视.*文件
      }
    ])
  ]
  • 额外配置
代码语言:javascript复制
if (config.build.productionGzip) {  // 配置文件开启gzip压缩
  var CompressionWebpackPlugin = require('compression-webpack-plugin')  // 引入压缩文件的组件,该插件会对生成的文件进行压缩,生成一个.gz文件

  webpackConfig.plugins.push(
    new CompressionWebpackPlugin({
      asset: '[path].gz[query]',  // 目标文件名
      algorithm: 'gzip', // 使用gzip压缩
      test: new RegExp( // 满足正则表达式的文件会被压缩
        '\.('  
        config.build.productionGzipExtensions.join('|')  
        ')$'
      ),
      threshold: 10240,  // 资源文件大于10KB的时候会被压缩
      minRatio: 0.8  // 最小压缩比达到0.8的时候会被压缩
    })
  )
}
代码语言:javascript复制
npm run dev

当我们熟悉了上面的配置之后,下面让我们来看看当我们执行了npm run dev之后发生了什么。 在package.json的文件中我们定义了dev的运行脚本

代码语言:javascript复制
  "scripts": {
    "dev": "node build/dev-server.js",
    "build": "node build/build.js"
  }

当运行 npm run dev的命令时,实际上会运行 dev-server.js文件。 下面我们来看看这个文件的构成:

  • 原来的版本
代码语言:javascript复制
require('./check-versions')()  // 检查版本

var config = require('../config')  // nodejs环境配置
if (!process.env.NODE_ENV) {
  process.env.NODE_ENV = JSON.parse(config.dev.env.NODE_ENV)
}

var opn = require('opn')  // 强制打开浏览器
var path = require('path')  // 提供文件路径的方法
var express = require('express')  // 借助express来启动服务
var webpack = require('webpack')
var proxyMiddleware = require('http-proxy-middleware') // http代理中间件
var webpackConfig = require('./webpack.dev.conf') // 依赖开发配置

// default port where dev server listens for incoming traffic
var port = process.env.PORT || config.dev.port // 端口号
// automatically open browser, if not set will be false
var autoOpenBrowser = !!config.dev.autoOpenBrowser  // 是否打开浏览器
// Define HTTP proxies to your custom API backend
// https://github.com/chimurai/http-proxy-middleware
var proxyTable = config.dev.proxyTable  // http的代理url

var app = express(); // 启动express
var compiler = webpack(webpackConfig);  // webpack编译

// devMiddleware这个中间件是express专门为webpack开发的中间件
// webpack-dev-middleware的作用
// 1.将编译后的生成的静态文件放在内存中,所以在npm run dev后磁盘上不会生成文件
// 2.当文件改变时,会自动编译。
// 3.当在编译过程中请求某个资源时,webpack-dev-server不会让这个请求失败,而是会一直阻塞它,直到webpack编译完毕
var devMiddleware = require('webpack-dev-middleware')(compiler, {
  publicPath: webpackConfig.output.publicPath,
  quiet: true
});

// webpack-hot-middleware的作用就是实现浏览器的无刷新更新,这个中间件是webpack中的一个小亮点
var hotMiddleware = require('webpack-hot-middleware')(compiler, {
  log: () => {}
});
// force page reload when html-webpack-plugin template changes
//声明hotMiddleware无刷新更新的时机:html-webpack-plugin 的template更改之后
compiler.plugin('compilation', function (compilation) {
  compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) {
    hotMiddleware.publish({ action: 'reload' })
    cb()
  });
});

// proxy api requests
//将代理请求的配置应用到express服务上
Object.keys(proxyTable).forEach(function (context) {
  var options = proxyTable[context]
  if (typeof options === 'string') {
    options = { target: options }
  }
  app.use(proxyMiddleware(options.filter || context, options))
})

// handle fallback for HTML5 history API
//使用connect-history-api-fallback匹配资源
//如果不匹配就可以重定向到指定地址
app.use(require('connect-history-api-fallback')())

app.use(devMiddleware)  // 应用devMiddleware中间件
app.use(hotMiddleware)  // 应用hotMiddleware中间件

// 配置express静态资源目录
var staticPath = path.posix.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory)
app.use(staticPath, express.static('./static'))

var uri = 'http://localhost:'   port  

devMiddleware.waitUntilValid(function () {  //编译成功后打印uri
  console.log('> Listening at '   uri   'n')
})

module.exports = app.listen(port, function (err) {  // 启动express服务
  if (err) {
    console.log(err)
    return
  }

  // when env is testing, don't need open it
 // 满足条件则自动打开浏览器
  if (autoOpenBrowser && process.env.NODE_ENV !== 'testing') {
    opn(uri)
  }
})
  • 现在的版本 现在 devServer可以直接配置在webpack里面了。
代码语言:javascript复制
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: config.dev.proxyTable,
    quiet: true, // necessary for FriendlyErrorsPlugin
    watchOptions: {
      poll: config.dev.poll,
    }
  },

替代上面一大串自定义express的代码

由于在package.json中的配置,npm run build执行的是build.js文件。

代码语言:javascript复制
require('./check-versions')()

process.env.NODE_ENV = 'production' // 设置当前的环境为production

var ora = require('ora') // 终端显示的转轮loading
var rm = require('rimraf') // node环境下rm -rf的命令库
var path = require('path') // 文件路径处理
var chalk = require('chalk')  // 终端显示带颜色的文字
var webpack = require('webpack')
var config = require('../config')
var webpackConfig = require('./webpack.prod.conf')  // 生产环境下的webpack配置

var spinner = ora('building for production...')  // 在终端显示ora库的loading效果
spinner.start()

// 删除已编译文件
rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => {
  if (err) throw err
//在删除完成的回调函数中开始编译
  webpack(webpackConfig, function (err, stats) {
    spinner.stop()  // 停止loading
    if (err) throw err
    // 在编译完成的回调函数中,在终端输出编译的文件
    process.stdout.write(stats.toString({
      colors: true,
      modules: false,
      children: false,
      chunks: false,
      chunkModules: false
    })   'nn')

    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'
    ))
  })
})

0 人点赞