vue-cli3.0的配置与优化

Vue 作为前端目前三大框架之一,相信大家都不陌生。在国内,由于 Vue 使用简便、学习成本低以及中文资料丰富,深受大量中小型公司的青睐。
Vue 创建项目是使用 vue-cli,目前 vue-cli 已经出到了 3.0 版本,这版本项目结构相当清晰,而且把大量的配置都封装成 @vue/cli-service 包里面,几乎成为开箱即用的脚手架。
然而,需求千变万化,不可能每一个项目都能够在 vue-cli 创建项目之后,就马上运用到业务场景,需要对它进行合适的优化以及修改。
Vue Cli 3.0 新建项目
用 vue-cli 创建项目,看起来十分简单。安装依赖,运行命令就可以了:
$ npm install -g @vue/cli

OR

$ yarn global add @vue/cli
$ vue create hello-world

然而对 vue-cli 配置的优化,就是从这里开始的。首先,我们需要选择自己手动选择配置:

Vue CLI v3.11.0
? Please pick a preset: Manually select features
? Check the features needed for your project: (Press <space> to select, <a> to toggle all, <i> to invert selection)
>(*) Babel
( ) TypeScript
( ) Progressive Web App (PWA) Support
( ) Router
( ) Vuex
( ) CSS Pre-processors
(*) Linter / Formatter
( ) Unit Testing
( ) E2E Testing

这里需要根据自己的业务需求来选择,我在做项目的时候,基本上必须选择的都是:Babel、Router、Vuex、Linter。其余配置,可以根据自己的需求选择。
选择完之后,会问你是否使用 History Mode 的路由,其实就是路径上面是否出现 #
我个人建议,不要选择 History Mode,因为 # 能够帮你避免很多错误的出现。特别是当项目没有前后端分离地十分彻底的情况下,直接跟路由,可能导致跳转到其他页面。
? Use history mode for router? (Requires proper server setup for index fallback in production) No

然后会选择 Linter 并且选择代码风格,我选择的是 ESLint + Standard,然后选择 Lint on save。

? Pick a linter / formatter config: Standard
? Pick additional lint features: (Press <space> to select, <a> to toggle all, <i> to invert selection)
>(*) Lint on save
( ) Lint and fix on commit
最后,会问你需要把配置写到单独的文件里面还是写到 package.json 里面,这里选择写到单独文件里面。
原因是我们需要去调整这些配置,如果写到 package 中,不方便调整。
? Where do you prefer placing config for Babel, PostCSS, ESLint, etc.?
> In dedicated config files
  In package.json
回车后,会生成下面的目录结构
+--- .browserslistrc
+--- .editorconfig
+--- .eslintrc.js
+--- .gitignore
+--- babel.config.js
+--- package.json
+--- postcss.config.js
+--- public
|   +--- favicon.ico
|   +--- index.html
+--- README.md
+--- src
|   +--- App.vue
|   +--- assets
|   |   +--- logo.png
|   +--- components
|   |   +--- HelloWorld.vue
|   +--- main.js
|   +--- router.js
|   +--- store.js
|   +--- views
|   |   +--- About.vue
|   |   +--- Home.vue
+--- yarn.lock

Vue Cli 3.0 配置 vue.config.js

我们观察一下这里面的目录,发现没有 webpack 的配置,并不是 3.0 版本没有使用 webpack,而是它把 webpack 封装起来了。那么我们怎么用去调整 webpack 配置呢?3.0 版本提供了一个 vue.config.js 的途径。

publicPath

PublicPath 是 webpack 中十分重要的一个配置,这个配置决定了生成资源的路径。我会把 publicPath 在 .env 中配置,development 的时候,配置成 ./;production 模式下,配置成生产环境的路径。

module.exports = {
  publicPath: process.env.PUBLIC_PATH
}

chainWebpack 与 configureWebpack

这两个都可以用来调整 webpack 配置,区别在于 configureWebpack 是简单通过 webpack-merge 对 webpack 进行扩展,而 chainWebpack 则是通过 webpack-chain 插件进行扩展。
具体用哪一个,其实我觉得都可以,看哪一个方便以及熟悉程度。
configureWebpack
configureWebpack 的配置使用正常赋值的形式,往 config 对象中添加或者修改数值即可:

  configureWebpack: config => {
    if (process.env.NODE_ENV === 'production') {
      // 为生产环境修改配置...
      config.mode = 'production'
    } else {
      // 为开发环境修改配置...
      config.mode = 'development'
    }
    Object.assign(config, {
      externals,
      // 开发生产共同配置
      resolve: {
        extensions: ['.js', '.vue', '.json'],
        alias: {
          '@': path.resolve(__dirname, './src')
        }
      }
    })
  },

chainWebpack
chainWebpack 的配置则是使用链式操作,通过调用方法来进行扩展:

  chainWebpack: config => {
    config.resolve.symlinks(true)
    config.module
      .rule('vue')
      .use('vue-loader')
      .loader('vue-loader')
      .tap(options => {
        options.transformAssetUrls = {
          audio: 'src'
        }
        return options
      })
  },

alias

使用别名是 Vue 开发中常用的技巧,如果大家也讨厌每次都要写无数个 ../ 的话,alias 是必备的配置。
alias: {
  '@': path.resolve(__dirname, './src')
}
配置了之后,在 Vue 的模板中,可以使用 @ 进行路径的引用:
<audio preload="preload" ref="audio" src="@/assets/music.mp3" loop="true" class="hidden"></audio>
import loading from '@/components/loading'
注意 scss 中的图片路径,需要使用 ~@
background: url(~@/assets/index-bg.jpg) no-repeat 0 0/100% 100%;

CSS

CSS 配置的是 Sass 或者 Scss 的属性,其中重要的是 loaderOptions,一般用来定义全局引入的 Scss:
  css: {
    // 是否使用css分离插件 ExtractTextPlugin
    extract: true,
    // 开启 CSS source maps?
    sourceMap: false,
    // css预设器配置项
    loaderOptions: {
      sass: {
        data: `@import "~@/scss/resources.scss";`
      }
    },
    // 启用 CSS modules for all css / pre-processor files.
    modules: true
  },

devServer

devServe 用来配置 webpack-dev-serve 的属性,可以把 host 配置成 0.0.0.0,这样能够以 localhost 以及电脑 IP 访问项目。

devServer: {
    host: '0.0.0.0',
    port: 3000,
    https: false,
    disableHostCheck: true
}

external

配置完这些基本东西之后,需要配置 external。这个属性在 webpack 中十分的重要而且常用,是用来定义外部引入的对象,定义了之后 webpack 就不会把这个对象对应的包打包进去项目文件中。
例如,我们在几个项目中都用到了 Vue,如果不配置 external,每个项目里面都会有 webpack 打包进来的 Vue 包,这样是非浪费服务器资源,而且加载文件大小也相对较大。
而如果配置了 external,我们就可以每次都加载同一个资源路径的 Vue,webpack 不会把 Vue 打包进去。
那怎么样去设置 external 呢?

以加载 Vue 为例,首先我们要在项目中引入 vue.min.js:

<script type="text/javascript" src=".../vue.min.js"></script>
然后,我们需要打开这个 vue.min.js,看一下里面暴露的对象是什么:
/*!
* Vue.js v2.6.10
* (c) 2014-2019 Evan You
* Released under the MIT License.
*/
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e=e||self).Vue=t()}(this,function(){"use strict";var e=Object.freeze({});function t(e){return null==e}function n(e){return...
一般我们打开之后,就看前两行或者最后两行,看到有 e.Vue 或者 module.Vue 之类的,这个 Vue 就是我们要的对象名字了。
拿到这个对象名称之后,建议在 vue.config.js 的顶部,定义一个 external 对象,来专门负责这些 externals:
const externals = {
  Swal: 'Swal',
  vue: 'Vue',
  vuex: 'Vuex',
  'vue-router': 'VueRouter',
  axios: 'axios',
  qs: 'Qs',
  Mock: 'Mock',
  TweenMax: 'TweenMax',
  TimelineMax: 'TimelineMax',
  AlloyTouch: 'AlloyTouch',
  Transform: 'Transform',
  html2canvas: 'html2canvas'
}
然后在 configureWebpack 中把 external 配置到 webpack 中:
  configureWebpack: config => {
    Object.assign(config, {
      externals,
    })
  },

Vue Cli 3.0 配置 .env

在开始配置的时候,一般会先在根目录新建两个文件:.env.development 以及 .env.production。这两个文件的作用是,当我们使用不同的 mode( development 和 production ) 去构建项目的时候,能够用 process.env 来获取对应文件中定义的配置。
.env.development
VUE_APP_SERVE=true
PUBLIC_PATH=./
VUE_APP_STATIC_PATH=https://www.bootcdn.cn/
注意一点,如果需要把 .env 里面的变量用到项目中,例如 public 中的 index.html 需要在前面加上 VUE_APP。具体的 .env 配置,可以在官网中的 API 查看。
使用这个配置文件有几个好处:
  1. 不需要查看具体代码就能方便修改里面的信息

  2. 方便同事或者其他开发人员查看配置详情

  3. 能够很好地区分开发环境与生成环境

  4. 定了文件之后,能够在项目任何地方使用里面定义的变量

Vue Cli 3.0 配置页面模板

说完上面的配置之后,其实之前所做的很多工作,都是为了配置页面模板所打下的基础。
页面模板是指 public 文件夹中的 index.html 文件,这个 index.html 比普通的 HTML 文件功能强大很多。它是一个会被 html-webpack-plugin 处理的模板,能够使用很多模板插值,条件渲染的语法。

插值

前面说到我们在 .env 中定义了一些静态资源的路径,这里就能够用上了:
<script type="text/javascript" src="<%= VUE_APP_STATIC_PATH %>/vue.min.js"></script>
<script type="text/javascript" src="<%= VUE_APP_STATIC_PATH %>/axios.min.js"></script>
<script type="text/javascript" src="<%= VUE_APP_STATIC_PATH %>/qs.min.js"></script>
使用 VUE_APP_ + 所以定义的变量名,就可以拿到对应的 .env 中的值,这样我们就不需要在 index.html 中写上详细的资源地址,只需要写上后缀就行。

表达式

除了能够插值之外,另一个常用的就是表达式,可以在页面上面使用 if else。我们可以在 .env 中配置一个属性,用来区分当前的状态是开发中,还是线上。然后对应不同的状态,加载不同的资源:

<% if (VUE_APP_SERVE === 'false') { %>
...
<% } else { %>
...
<% } %>

模拟数据

除了使用简单的插值跟表达式以外,前端开发经常需要处理的是模拟数据的情况。在后端没有给我们足够的数据支持的时候,我们需要自己去模拟数据以便测试我们的功能页面。
这里我们可以利用 API 工具加上页面模板,来实现模拟数据。
首先,我们需要找到一款合适的 API 工具,现在有很多开源的文档工具例如:apiDoc、Swagger 和 ShowDoc 等。不过这些都需要团队去学习如何使用,并且需要搭建环境。
我最终选择了 Postman,Postman 中有一个 Mock 功能,这个功能允许我们做数据模拟。

由于后端开发人员几乎都会使用 Postman 来测试自己的接口,所以我们只需要让后端同事导出一份 Postman 的文件,我们导入就有了对应项目的所有接口。
接下来我们要做的是,新建各种例子,然后在 Mock 的地址中,测试一下是否配置成功:

配置好 Postman 之后,我们就可以在页面模板中,把这些接口加入到 Vue 对象里面:
Vue.prototype.$projectOptions = {
  addUserInfo: '<%= VUE_APP_POSTMAN %>/addUserInfo',
}
然后就可以在项目中,用 this.$projectOptions.addUserInfo 来请求 Postman 中我们定义的接口了。
页面模板中还有很多可以优化的配置,像 Preload 和 Prefetch 等等,这些大家可以根据情况来使用,这里就不一样展开了。

结尾

最后给大家一些配置优化的小 Tips:
  • vue-cli 的配置大部分跟 webpack 有关,用好 webpack 就能对 vue-cli 的配置得心应手

  • vue-cli 已经是一个成熟的脚手架了,能够用官网提供的方法解决的情况下,就不用自己去找额外的 Loader 跟插件,避免不必要的麻烦

  • vue-cli 有模板功能,可以把自己配置好的文件弄成模板,下次新建项目的时候,直接用自己的模板 

JavaScript 基础强化:函数的应用(高阶函数)

高阶函数

所谓的高阶函数从通俗的意义上说就是以下两点满足任何一个都是高阶函数:

  • 一个函数的参数是函数 

(){}
(()=>{})
  • 一个函数返回一个函数

(){
    (){}
}

从我们的经验上可以得知,第一种参数是函数的,我们称之为回调方法。第二种返回一个函数的我们可以叫他拆分方法(把一个功能复杂的函数拆分出去) 结合上面两种定义,我们写一个封装功能时常用的例子

before方法:我们希望在调用一个方法之前先调用before函数,然后在调用核心功能函数(一步一个注释,像我这么细心的老大哥可不多咯)

= ()=>{
    console.() }
newCore = .(()=>{
    console.()
})
newCore1 = .(()=>{
    console.()
})

要实现上面的功能的代码其实很简单

Function..= (beforeFn){ ()=>{ beforeFn()
        () }
}

接下来就可以看看效果了,全部是这样的如下:

Function..= (beforeFn) { () => {    beforeFn()()}}= () => {
    console.()}newCore = .(() => {
    console.()})newCore1 = .(() => {
    console.()})newCore()newCore1()

什么?你还想传递参数啊?哎呀 你可真是个小机灵鬼 下面是传递参数的版本

Function..= (beforeFn) {
    (...arg) => { beforeFn()(...arg)}}= (...arg) => { console.(arg)}newCore = .(() => {
    console.()})newCore1 = .(() => {
    console.()})newCore()newCore1()

发布订阅

趁热打铁,我们再来看一个示例,react的事务队列的原理。

可以在某件事的前面和后面同事增加方法,同时也是发布订阅的一种应用。既然说到发布订阅,那就简单说两句(就两句)

  • 特点一 预先定义好一个东西,等某个东西发生的时候执行(这大白话可以吧)

  • 特点二 发布 和 订阅 之间是没有关系的 好了,两句写完了(我也就能总结成这样了,再通俗的我也不知道咋写了)还可以用现实中的一些例子来理解一下, 如:我要在9点钟吃一个苹果

预先定义好一个东西: 吃一个苹果 (也就是订阅)

等某个东西发生再执行:9点钟 (也就是发布)

9点钟和吃一个苹果没有关系 (满足特点二)

遮掩是不是好理解一点???(大概吧 😯)

来说回代码,下面这个例子就是的发布订阅的简单应用了

= (anymethodwrappers) => { wrappers.(wrap => {
        wrap.()})anymethod()wrappers.(wrap => {
        wrap.()})}(() => {
    console.()}[
    {
        () { console.()}() {
            console.()}
    }{
        () { console.()}() {
            console.()}
    }
])

那我们再来看一下另一个例子,用发布订阅处理并发问题,但是在此之前先来看看计数器方式处理并发问题的例子了解一下这个故事的前因后果(想跟我发生故事吗?)

再看看优雅的实现方式,应用了高阶函数,可见高阶函数在实际应用中还是非常广泛的

// 先写一个after函数
const after = (times,fn) => () => --times === 0 && fn() // 返回一个方法 当参数times减少为0的时候执行回调方法
let info = {}
const out = after(2, () => console.log("优雅的", info));

fs.readFile("name.txt", "utf8", (err, data) => {
  info["name"] = data;
  out();
});
fs.readFile("age.txt", "utf8", (err, data) => {
  info["age"] = data;
  out();
});

现在故事情节了解的差不多了吧,我们来看看发布订阅的实现方式吧

// 用发布订阅的方式实现// 用on订阅,emit来发布实现
let e = {
  arr: [],  on(fn) {    this.arr.push(fn);
  },
  emit() {    this.arr.forEach(fn => fn());
  }
};
let info = {};
e.on(() => {  console.log("ok");
});// 想写多少个就写多少个
e.on(() => { // 订阅  if (Object.keys(info).length === 1) {    console.log("这个订阅会在length为1的时候发布");
  }
});
e.on(() => { // 订阅  if (Object.keys(info).length === 2) {    console.log("发布订阅实现", infoOnEmit);
  }
});
fs.readFile("name.txt", "utf8", (err, data) => {
  info["name"] = data;
  e.emit(); //  发布
});
fs.readFile("age.txt", "utf8", (err, data) => {
  info["age"] = data;
  e.emit();  //  发布
});

以上就是发布订阅的标准写法了,应付面试题应该足够了 on-订阅 emit-发布,台下的同学跳起来说了,“这个这个怎么跟Vue里的那个事件啥啥啥的那么像呢”,这位同学请坐下,你说的没错哦,vue里面也有很多发布订阅应用,但是不要急哦,我还没写到呢,后面会在本博客的源码篇里面陆续分析哦。(标签就叫源码)

函数柯里化

函数柯里化属于高阶函数,但什么时候函数柯里化呢,通俗点说就是把一个大函数拆分成多个函数(大概就是不停的返回函数)

真理还是要得实践嘛,所以咱们主要还是看例子,那我们就继续来了解一下故事情节好了

下面我们准备写一个类型判断的方法,一般的类型判断怎么实现呢?

Object.prototype.toString.call() 
// 随便试两个 就两个console.log(Object.prototype.toString.call("123")); // [object String]console.log(Object.prototype.toString.call([123])); // [object Array]

然后我们再看看一般的封装是怎么实现的

const checkType = (content, type) => {  return Object.prototype.toString.call(content) === `[object ${type}]`;
};const b = checkType(123, "Number");console.log(b); // true

功能实现了没有问题,好了故事结束了。 哎!!导演导演,剧本不是这样的!!

好吧 我还以为可以回家吃火锅了呢!

上面的一般封装实现了判断类型的功能是没错的,所有的类型都每次判断的时候手动写入如果写错了就会导致错误,像我这种手残的就很容易敲错啊,老人家太难了,我太难了(尺神经损伤已经好几个月不能打球了,还要奋(划)斗(水)在一线战场搬砖)。

咳咳!说回猪蹄!呸!是主题。恩 主题!

所以我们要尽量不要每次判断都自己写类型,所以我们应用 函数柯里化 的时候到了,先来个基础版的尝尝鲜

// 柯里化实现(简单基础版)const checkType = type => {    return content => {        return Object.prototype.toString.call(content) === `[object ${type}]`
    }
}const isString = checkType('String') // 返回内层函数console.log(isString("123")) // true

到这里我成功应用了函数柯里化把一个方法拆分成了多个方法(这篇文章已经够长了,我就写一个String的就好了)且每个类型就写了一遍,减少了犯错几率

“这也太麻烦了吧,那么多个类型我要写那么多啊,好烦的啊,我很懒的啊”,好这位同学你没有错,错的是这个世界,懒才是我们的第一动力。

所以上面是基础版嘛,简单补充一下就是下面这个样子了

const checkType = type => {    return content => {        return Object.prototype.toString.call(content) === `[object ${type}]`
    }
}const utils = {} // 声明一个工具方法对象const TYPES = ["Number","String","Object","Array","Boolean"] // 行了行了 手疼TYPES.forEach( type => {
    utils[`is${type}`] = checkType(type)
})

也很简单是吧。哎呀!代码可真是太好玩了,我的天啊。

观察者模式

观察者模式的特点有三个

  • 观察者和被观察者是有联系的

  • 被观察者里面存了观察者

  • 观察者模式包含发布订阅

继续看码了,我敲代码千百遍,代码对我如初见~,当然这种事不会的,正所谓码敲百遍,其意自现。没事多敲敲,肯定好处多多啊

// 被观察者class Subject {  constructor() {    this.arr = [];    this.state = "我不饿";
  }  //  通过这个方法将观察者存入arr
  attach(o) { 
    this.arr.push(o);
  }
  setState(newState) { // 通过这个方法来设置状态并通知观察者
    this.state = newState;    this.arr.forEach(o => o.update(newState));
  }
}// 观察者class Observer {  constructor(name) {    this.name = name;
  }
  update(newState) { // 通过这个方法来更新到被观察者的状态
    console.log(`${this.name}  知道了 九儿  ${newState}`);
  }
}let o1 = new Observer("Mopecat"); // 创建一个观察者 Mopecat(我)let o2 = new Observer("Sean");  // 创建一个观察者 Sean(我媳妇)let s = new Subject("九儿"); // 九儿是我家的猫s.attach(o1); // 将o1存入被观察者 也就是Mopecats.attach(o2); // 将o2存入被观察者 也就是Seans.setState("又饿了"); // 更新状态 // 会输出Mopecat  知道了 九儿  又饿了// Sean  知道了 九儿  又饿了

上面就是也一个简单的观察者模式的例子

其中向被观察者中存入观察者的直到状态更新的时候再通知观察者的过程就是发布订阅的应用

而被观察者和观察者是有联系的:被观察者中存了观察者。好像上面的三个特点改成两个就可以了。算了就这吧