懒人改变世界
用过Vue.js
的同学对vue-cli
一定都不陌生,借助vue-cli
我们可以通过问答的形式,方便地初始化一个 vue 工程,完全不用担心繁琐的 webpack、eslint 配置等。
什么是vue-cli
引用 vue-cli 官方文档的一句话:
A simple CLI for scaffolding Vue.js projects.
一个简单的 Vue.js 工程命令行脚手架工具。
在全局安装 vue-cli 之后,就可以通过一条命令初始化我们的 vue 工程:
1 | $ vue init <template-name> <project-name> |
接下来 vue-cli 就会按照这个<template-name>
模板内部的设置,抛出几个问答选项。在回答完问答选项以后,我们的 vue 工程目录就已经生成好了,接下来只要把依赖安装完,直接就可以跑起来,是不是非常方便呢?
接下来,我们就要看看,这一条命令的背后,究竟发生了一些什么事。
vue-cli 初始化项目的原理
从官方文档可以知道,vue-cli 使用了download-git-repo这个工具去下载远端 git 仓库里面的工程,如果加上了--clone
参数,则会在内部运行git clone
,通过克隆的方式把远端 git 仓库拉取到本地。明白这一点至关重要:
vue-cli 并非从无到有地凭空生成一个项目,而是通过下载/拉取已有的工程到本地,完成生成项目的工作。
而这个“已有的工程”,就是所谓的“模板(template)”。
当然,vue-cli 可不只是把模板拉取到本地这么简单,它还允许我们通过问答的形式对模板进行个性化配置,这个又是如何做到的呢?
vue-cli 使用了inquirer.js实现了“问答环节”,简单来说是这样子的:
1 | // 准备几个问题 |
然后把这段问题传给 inquirer.js 就可以了:
1 | inquirer.prompt(questions).then(({ name, age }) => { |
在运行的时候,vue-cli 会在命令行里面把What's your name?
和How old are you?
这两个问题相继抛出,获取用户输入,把输入赋值给name
和age
变量,这样就能够获取用户的输入信息了。接着我们引出下一个问题,这些用户输入,是如何跟模板的自定义关联起来的呢?
我们打开一个 vue-cli 的模板,比如webpack-simple 里面的 README.md,它长这样:
1 | # {{ name }} |
上面使用双括号包裹起来的,最终会根据用户的输入而更改为具体的内容。是不是觉得这种写法很熟悉?其实就是Handlebars的模板语法。
以这个 README.md 文件为例,在 vue-cli 运行的过程中,会首先读取文件的内容放在内存,然后通过inquirer.js
获取用户输入,把输入的值替换到文件内容里面,最后通过另外一个名叫Metalsmith的工具,把替换好的内容输出为文件,也就生成了具有个性化内容的 README.md 文件了。
整个流程并不复杂,在明白这些原理后,我们就能更深入地使用 vue-cli 了。
vue-cli 与 vue
vue-cli 不仅仅能初始化 vue 工程,理论上能够初始化一切工程,包括 react,angular 等等等等,只要你有一份能够运行的模板,就能够通过 vue-cli 进行工程的初始化。
在讨论区有许多类似的问题:
- “vue-cli 当中如何配置 sass?”
- “vue-cli 中如何修改 devServer 的端口?”
- “vue-cli 中发现项目跑不起来”
- ……
vue-cli 说:“这锅我不背。”
是的,所遇到的问题都不是 vue-cli 的问题,而是相关模板的问题。那么应该如何写一份合格的模板呢?下面我们一起来研究一下。
写一份 vue-cli 模板
参考官方文档,也许还是不能理解到底应该怎么写,那么我们可以直接参考官方例子vuejs-templates/webpack,看看它到底是怎么写的。
初始化项目
先全局安装 vue-cli 脚手架工具:
1
$ npm install -g vue-cli
如果喜欢尝鲜的可以使用最新版的
@vue/cli 3.0
@vue/cli 3.0
默认是没有根据模板 init 项目的,不过官方提供了一个插件@vue/cli-init
1
2
3$ npm install -g @vue/cli
# or
$ yarn global add @vue/cli1
2# vue init now works exactly the same as [email protected]
$ npm install -g @vue/cli-init
安装完成后,初始化基于 webpack
的项目模板,按照指示依次填写项目信息和选择需要的模块:
1 | $ vue init webpack vue-pro-demo |
执行完成后,当前目录下就会生成命名为 vue-pro-demo
的项目文件夹,结构如下:
1 | . |
对于 src
目录,我们在开发中也会根据文件的功能进行文件夹拆分,比如我个人比较喜欢的结构如下(仅供参考):
1 | . |
规范的目录结构可以很好的规范化你的开发习惯,代码分工明确,便于维护。
定制开发项目模板
每个人在使用官方项目模板开发项目的时候,都或多或少的会修改一些默认的 webpack
配置,然后添加一些项目经常使用的的插件,也会根据自己需要在 src
目录下添加一些通用的文件夹目录,比如上面所说到的。
这就存在一个问题,每次我们在初始化项目的时候,都需要重复完成这几项操作,作为一个懒癌晚期患者的程序员,又怎么能容忍此类事情发生呢?所以定制化的需求蠢蠢欲动了。
下面就介绍下如何对官方的 webpack 模板 进行二次开发。
二次开发
要做到这点,只需要三步:
- Fork 官方源码 vuejs-templates/webpack
- 克隆到本地二次开发,添加自己想要的配置和插件,并 push 到 github
- 初始化项目时,使用我们自己的仓库就行
对于步骤 1,会使用 github 的朋友应该都没问题。
接下来,重点介绍下步骤 2。
克隆项目vuejs-templates/webpack到我们的本地后,你会发现目录结构是这样的:
1 | . |
这里我们只需要关心meta.js
和template
目录就够了,meta.js
用来配置问答信息,template
目录存放的就是我们的项目模板。
打开 template/src/main.js
文件(项目入口文件),代码如下:
1 | {{#if_eq build "standalone"}} |
其中包含了很多 Handlebars 的语法,这里主要用到了 if
条件判断语法,也很好理解。
然后就可以按照官方的模板照猫画虎修改自己的模板配置。
修改
template
模板文件首先我们对
template/package.json
做些调整,添加 vuex、axios、qs、pug、scss……依赖。最终修改完成的 package.json 文件如下。有可能一些小项目不需要 vuex,所以我对 vuex 添加
if
条件判断。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141{
"name": "{{ name }}",
"version": "1.0.0",
"description": "{{ description }}",
"author": "{{ author }}",
"private": true,
"scripts": {
"dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js",
"start": "npm run dev",
{{#if_eq runner "jest"}}
"unit": "jest --config test/unit/jest.conf.js --coverage",
{{/if_eq}}
{{#if_eq runner "karma"}}
"unit": "cross-env BABEL_ENV=test karma start test/unit/karma.conf.js --single-run",
{{/if_eq}}
{{#e2e}}
"e2e": "node test/e2e/runner.js",
{{/e2e}}
{{#if_or unit e2e}}
"test": "{{#unit}}npm run unit{{/unit}}{{#unit}}{{#e2e}} && {{/e2e}}{{/unit}}{{#e2e}}npm run e2e{{/e2e}}",
{{/if_or}}
{{#lint}}
"lint": "eslint --ext .js,.vue src{{#unit}} test/unit{{/unit}}{{#e2e}} test/e2e/specs{{/e2e}}",
{{/lint}}
"build": "node build/build.js"
},
"dependencies": {
"vue": "^2.5.2",
{{#vuex}}
"vuex": "^3.0.1",
{{/vuex}}
{{#router}}
"vue-router": "^3.0.1",
{{/router}}
"axios": "^0.18.0",
"qs": "^6.5.1"
},
"devDependencies": {
{{#lint}}
"babel-eslint": "^7.2.3",
"eslint": "^4.15.0",
"eslint-friendly-formatter": "^3.0.0",
"eslint-loader": "^1.7.1",
"eslint-plugin-vue": "^4.0.0",
{{#if_eq lintConfig "standard"}}
"eslint-config-standard": "^10.2.1",
"eslint-plugin-promise": "^3.4.0",
"eslint-plugin-standard": "^3.0.1",
"eslint-plugin-import": "^2.7.0",
"eslint-plugin-node": "^5.2.0",
{{/if_eq}}
{{#if_eq lintConfig "airbnb"}}
"eslint-config-airbnb-base": "^11.3.0",
"eslint-import-resolver-webpack": "^0.8.3",
"eslint-plugin-import": "^2.7.0",
{{/if_eq}}
{{/lint}}
{{#if_eq runner "jest"}}
"babel-jest": "^21.0.2",
"babel-plugin-dynamic-import-node": "^1.2.0",
"babel-plugin-transform-es2015-modules-commonjs": "^6.26.0",
"jest": "^22.0.4",
"jest-serializer-vue": "^0.3.0",
"vue-jest": "^1.0.2",
{{/if_eq}}
{{#if_eq runner "karma"}}
"cross-env": "^5.0.1",
"karma": "^1.4.1",
"karma-coverage": "^1.1.1",
"karma-mocha": "^1.3.0",
"karma-phantomjs-launcher": "^1.0.2",
"karma-phantomjs-shim": "^1.4.0",
"karma-sinon-chai": "^1.3.1",
"karma-sourcemap-loader": "^0.3.7",
"karma-spec-reporter": "0.0.31",
"karma-webpack": "^2.0.2",
"mocha": "^3.2.0",
"chai": "^4.1.2",
"sinon": "^4.0.0",
"sinon-chai": "^2.8.0",
"inject-loader": "^3.0.0",
"babel-plugin-istanbul": "^4.1.1",
"phantomjs-prebuilt": "^2.1.14",
{{/if_eq}}
{{#e2e}}
"babel-register": "^6.22.0",
"chromedriver": "^2.27.2",
"cross-spawn": "^5.0.1",
"nightwatch": "^0.9.12",
"selenium-server": "^3.0.1",
{{/e2e}}
"autoprefixer": "^7.1.2",
"babel-core": "^6.22.1",
"babel-helper-vue-jsx-merge-props": "^2.0.3",
"babel-loader": "^7.1.1",
"babel-plugin-syntax-jsx": "^6.18.0",
"babel-plugin-transform-runtime": "^6.22.0",
"babel-plugin-transform-vue-jsx": "^3.5.0",
"babel-preset-env": "^1.3.2",
"babel-preset-stage-2": "^6.22.0",
"chalk": "^2.0.1",
"copy-webpack-plugin": "^4.0.1",
"css-loader": "^0.28.0",
"extract-text-webpack-plugin": "^3.0.0",
"file-loader": "^1.1.4",
"friendly-errors-webpack-plugin": "^1.6.1",
"html-webpack-plugin": "^2.30.1",
"webpack-bundle-analyzer": "^2.9.0",
"node-notifier": "^5.1.2",
"node-sass": "^4.7.2",
"postcss-import": "^11.0.0",
"postcss-loader": "^2.0.8",
"postcss-url": "^7.2.1",
"pug": "^2.0.1",
"pug-loader": "^2.3.0",
"sass-loader": "^6.0.7",
"semver": "^5.3.0",
"shelljs": "^0.7.6",
"optimize-css-assets-webpack-plugin": "^3.2.0",
"ora": "^1.2.0",
"rimraf": "^2.6.0",
"uglifyjs-webpack-plugin": "^1.1.1",
"url-loader": "^0.5.8",
"vue-loader": "^13.3.0",
"vue-style-loader": "^3.0.1",
"vue-template-compiler": "^2.5.2",
"portfinder": "^1.0.13",
"webpack": "^3.6.0",
"webpack-dev-server": "^2.9.1",
"webpack-merge": "^4.1.0"
},
"engines": {
"node": ">= 6.0.0",
"npm": ">= 3.0.0"
},
"browserslist": [
"> 1%",
"last 2 versions",
"not ie <= 8"
]
}修改入口文件如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71/*
* _ooOoo_
* o8888888o
* 88" . "88
* (| -_- |)
* O\ = /O
* ____/`---'\____
* .' \\| |// `.
* / \\||| : |||// \
* / _||||| -:- |||||- \
* | | \\\ - /// | |
* | \_| ''\---/'' | |
* \ .-\__ `-` ___/-. /
* ___`. .' /--.--\ `. . __
* ."" '< `.___\_<|>_/___.' >'"".
* | | : `- \`.;`\ _ /`;.`/ - ` : | |
* \ \ `-. \_ __\ /__ _/ .-` / /
* ======`-.____`-.___\_____/___.-`____.-'======
* `=---='
* ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
* 佛祖保佑 永无BUG
*/
{{#if_eq build "standalone"}}
// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
{{/if_eq}}
import Vue from 'vue'
import App from './App'
{{#router}}
import router from './router'
{{/router}}
{{#vuex}}
import store from './store'
{{/vuex}}
Vue.config.productionTip = false
Vue.nextTick(() => {
if (window.addEventListener) {
const html = document.documentElement
let setFont = () => {
const k = 750
html.style.fontSize = html.clientWidth / k * 32 + 'px'
}
setFont()
setTimeout(function () {
setFont()
}, 300)
document.addEventListener('DOMContentLoaded', setFont, false)
window.addEventListener('resize', setFont, false)
window.addEventListener('load', setFont, false)
}
})
/* eslint-disable no-new */
new Vue({
el: '#app',
{{#router}}
router,
{{/router}}
{{#vuex}}
store,
{{/vuex}}
{{#if_eq build "runtime"}}
render: h => h(App)
{{/if_eq}}
{{#if_eq build "standalone"}}
components: { App },
template: '<App/>'
{{/if_eq}}
})修改
meta.js
例如上面的我在 main.js 中添加 vuex 的相关信息,但是有些小项目可能用不上 vuex,这是我们可以模仿官方的问答模式添加自己的问题,这里我选择询问是否安装 router 之后询问是否安装 vuex。
prompts
:问答列表。filters
:根据问答列表要过滤的文件夹。
例如我在 prompts 的 router 下一条添加 vuex ↓
1
2
3
4
5vuex: {
when: 'isNotTest',
type: 'confirm',
message: 'Install vuex?'
}如果选择了不需要 vuex 的,则对应不生成 store 文件夹,所以在 filters 里添加 ↓
1
'src/store/**/*': 'vuex'
完整的
meta.js
修改如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195const path = require('path')
const fs = require('fs')
const { sortDependencies, installDependencies, runLintFix, printMessage } = require('./utils')
const pkg = require('./package.json')
const templateVersion = pkg.version
const { addTestAnswers } = require('./scenarios')
module.exports = {
metalsmith: {
// When running tests for the template, this adds answers for the selected scenario
before: addTestAnswers,
},
helpers: {
if_or(v1, v2, options) {
if (v1 || v2) {
return options.fn(this)
}
return options.inverse(this)
},
template_version() {
return templateVersion
},
},
prompts: {
name: {
when: 'isNotTest',
type: 'string',
required: true,
message: 'Project name',
},
description: {
when: 'isNotTest',
type: 'string',
required: false,
message: 'Project description',
default: 'A Vue.js project',
},
author: {
when: 'isNotTest',
type: 'string',
message: 'Author',
},
build: {
when: 'isNotTest',
type: 'list',
message: 'Vue build',
choices: [
{
name: 'Runtime + Compiler: recommended for most users',
value: 'standalone',
short: 'standalone',
},
{
name:
'Runtime-only: about 6KB lighter min+gzip, but templates (or any Vue-specific HTML) are ONLY allowed in .vue files - render functions are required elsewhere',
value: 'runtime',
short: 'runtime',
},
],
},
router: {
when: 'isNotTest',
type: 'confirm',
message: 'Install vue-router?',
},
vuex: {
when: 'isNotTest',
type: 'confirm',
message: 'Install vuex?',
},
lint: {
when: 'isNotTest',
type: 'confirm',
message: 'Use ESLint to lint your code?',
},
lintConfig: {
when: 'isNotTest && lint',
type: 'list',
message: 'Pick an ESLint preset',
choices: [
{
name: 'Standard (https://github.com/standard/standard)',
value: 'standard',
short: 'Standard',
},
{
name: 'Airbnb (https://github.com/airbnb/javascript)',
value: 'airbnb',
short: 'Airbnb',
},
{
name: 'none (configure it yourself)',
value: 'none',
short: 'none',
},
],
},
unit: {
when: 'isNotTest',
type: 'confirm',
message: 'Set up unit tests',
},
runner: {
when: 'isNotTest && unit',
type: 'list',
message: 'Pick a test runner',
choices: [
{
name: 'Jest',
value: 'jest',
short: 'jest',
},
{
name: 'Karma and Mocha',
value: 'karma',
short: 'karma',
},
{
name: 'none (configure it yourself)',
value: 'noTest',
short: 'noTest',
},
],
},
e2e: {
when: 'isNotTest',
type: 'confirm',
message: 'Setup e2e tests with Nightwatch?',
},
autoInstall: {
when: 'isNotTest',
type: 'list',
message: 'Should we run `npm install` for you after the project has been created? (recommended)',
choices: [
{
name: 'Yes, use NPM',
value: 'npm',
short: 'npm',
},
{
name: 'Yes, use Yarn',
value: 'yarn',
short: 'yarn',
},
{
name: 'No, I will handle that myself',
value: false,
short: 'no',
},
],
},
},
filters: {
'.eslintrc.js': 'lint',
'.eslintignore': 'lint',
'config/test.env.js': 'unit || e2e',
'build/webpack.test.conf.js': "unit && runner === 'karma'",
'test/unit/**/*': 'unit',
'test/unit/index.js': "unit && runner === 'karma'",
'test/unit/jest.conf.js': "unit && runner === 'jest'",
'test/unit/karma.conf.js': "unit && runner === 'karma'",
'test/unit/specs/index.js': "unit && runner === 'karma'",
'test/unit/setup.js': "unit && runner === 'jest'",
'test/e2e/**/*': 'e2e',
'src/router/**/*': 'router',
'src/store/**/*': 'vuex',
},
complete: function(data, { chalk }) {
const green = chalk.green
sortDependencies(data, green)
const cwd = path.join(process.cwd(), data.inPlace ? '' : data.destDirName)
if (data.autoInstall) {
installDependencies(cwd, data.autoInstall, green)
.then(() => {
return runLintFix(cwd, data, green)
})
.then(() => {
printMessage(data, green)
})
.catch(e => {
console.log(chalk.red('Error:'), e)
})
} else {
printMessage(data, chalk)
}
},
}
本地测试使用
好了,这样我们的自定义组件已经添加完成了,那么在提交之前,我们还需要先测试下修改后的模板是否有效,运行命令进行初始化:
1 | $ vue init path/to/webpack-template my-project |
这里 vue init
的第一个参数 path/to/webpack-template
就是当前修改后的模板路径,之后还是重复交互式的配置过程,然后运行你的项目就行了。
不出意外地话,你的项目会很顺利的 npm run dev
跑起来,之后我们只需要 push
到我们自己的 github
仓库就行了。
使用
提交以后,项目同事都可以共享这份模板了,用的时候只需要运行以下命令:
1 | $ vue init MrLeo/webpack my-project |
这里的
MrLeo/webpack
参数就是告诉vue-cli
在初始化的时候到用户MrLeo
的 github 仓库下载webpack
项目模板。
之后你就可以愉快的编写输出你的 Hello world
了。
补充说明
当你你足够熟悉项目模板,你也可以对 webpack
配置进行更个性化的配置,或者添加 vue init
时的交互式命令。感兴趣的可以参看下我的个人模板 MrLeo/webpack。
by yugasun from https://yugasun.com/post/you-dont-know-vuejs-9.html
by jrainlau from https://segmentfault.com/a/1190000011643581?_ea=2709729