这是针对 Vue 新人的一个简单指导。
在前面的文章中,我们不推荐新手直接使用 vue-cli,(尤其是在还不熟悉基于 node.js 的构建工具时),而是通过直接引用的方式:
<script src="https://unpkg.com/vue@next"></script>
随着页面中组件越写越多,单个 HTML 文件终究无法完成实际的整个 Vue 项目,同时为了对 Vue 的使用有个整体的了解,这里将展示如何通过脚手架工具创建 Vue 3 项目以及一些前置说明。
node.js 听起来似乎是某个 JavaScript 库,而实际上,根据 nodejs.org 的说法,这是一个基于 Chrome V8 引擎的 Javascript 运行环境。
一切都要从 JavaScript 说起。
JavaScript 出现已经好多年了,一种事件驱动的、跨平台的脚本语言,诞生之初,就被用在 Netscape Navigator 浏览器中操作网页,良好的事件驱动特性让 JavaScript 能轻松应对网页中的各种用户操作事件。
人们越来越对 JavaScript 感兴趣,无疑推动了 JavaScript 的发展,非常多安全的、异步的代码被贡献出来,使得它不仅仅能被用于在浏览器中操作网页,甚至还能胜任实现网络服务器、文件处理程序、图形处理程序、数据库驱动程序等等工作。
同时,随着 JavaScript 在网站前端代码中越来越多地承担一些逻辑工作以及各个浏览器对处理 JavaScript 性能的巨大提升,使得 JavaScript 更加受欢迎。
正是 JavaScript 的优秀表现,让一个名叫 Ryan Dahl 的帅哥在打算开发一个需要有非阻塞处理能力的网站时注意到它。
Ryan Dahl 就将 Chrome’s V8 JavaScript engine 与一些必要的 JavaScript 运行环境和 JavaScript 库封装在一起,于是,在2009年,node.js 诞生了。可以浏览下面的链接了解 Ryan Dahl 与 node.js 的内容:
node.js 的出现统一了网站开发中前后端的编程语言,让在服务器端运行 JavaScript 成为可能。但出人意料的是,它却成为前端开发中非常受欢迎的工具。
JavaScript能做什么,该做什么?
How Node.js works
尽管 node.js 的出现将前后端开发语言统一为 JavaScript,但这里我们只从前端开发的角度简单说一下为什么要在前端开发中使用 node.js。
前端开发和后端开发一样,已经是一项工程化、模块化的工作。
一个工程化的应用显然就也不是一两个文件就能搞定的,它不再是单页应用(SPA),而是一系列的对功能和服务做出支持作用的文件的集合。
因为编码的过程就是一个抽象的过程,为了实现抽象结果的高复用性,我们通常会将代码按照功能的的痛进行封装,成为各个函数、类、模块、包、库等,JavaScript 也不会例外。
正是代码之间的复杂的依赖关系,让开发人员急需一套能有效管理依赖并能协同工作的代码管理(尤其是第三方库)工具——包管理器。
在 node.js 中,最初的包管理器就是一个名为"pm"("pkgmakeinst"的简称)的 bash 实用程序——一个在各种平台上安装各种东西的 bash 函数。
后来这个默认的包管理器程序逐渐扩展为由一个命令行客户端(npm)和一个包含公共和付费私人软件包的在线数据库(npm registry)两部分组成的工具。这个包管理器就是 npm。
npm registry提供了许多包公使用,可在npm 客户端通过一系列的命令来安装、删除、更新项目所依赖的包。两者配合,形成了强大的依赖管理能力,提高了开发人员的工作效率。
可以这么说,就算没有 node.js 没有 npm,也会有 XXX,但正是 node.js 中的 npm 明显让前端开发人员免于应付复杂繁琐的包管理工作,这就是它在前端开发中最大的作用之一。
纯前端开发眼里nodejs到底是什么?
我们为什么要使用NodeJS
有很多方法可以安装 node.js,这里只介绍最简单的:通过可执行文件安装——访问 node.js下载页面下载不同操作系统的安装包,傻瓜式“下一步”。
回到教程,安装完成后,查看一下能否工作:
# 查看 node.js 版本
F:\web前端\vue\my-project>node -v
v12.18.3
先看看能否正常工作:
F:\web前端\vue\mytest\my-project>node -v
v12.18.3
F:\web前端\vue\mytest\my-project>npm -v
8.6.0
在项目目录中创建一个名为 first.js 的 JavaScript 文件:
// 引入 http 模块
var http = require("http");
// 创建http服务器
var server = http.createServer(function (req, res) {
// 设置响应头
res.setHeader('Content-Type', 'text/html;charset=utf-8');
// 设置响应状态码
res.statusCode = 200;
// 设置响应数据
res.end('Hello World
');
});
//运行服务器,监听8001端口
server.listen(8001, "127.0.0.1");
// 终端打印如下信息
console.log('Server running at http://127.0.0.1:8001/')
通过 node.js 启动文件:
F:\web前端\vue\mytest\my-project>node first.js
Server running at http://127.0.0.1:8001/
访问 http://127.0.0.1:8001/,结果如下:
详细的使用教程,请参考官方文档。有几个重点需要关注:
可以跟着使用 Node.js 构建 JavaScript 应用程序来实际体验。
回到教程。
1,先将包管理器切换为 nrp。
# 查看 npm 版本
F:\web前端\vue\my-project>npm -v
8.6.0
# 安装 nrm
F:\web前端\vue\my-project>npm install nrm -g
npm WARN deprecated [email protected]: this library is no longer supported
npm WARN deprecated [email protected]: Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.
npm WARN deprecated [email protected]: request has been deprecated, see https://github.com/request/request/issues/3142
added 58 packages, and audited 316 packages in 14s
13 packages are looking for funding
run `npm fund` for details
3 moderate severity vulnerabilities
To address all issues, run:
npm audit fix
Run `npm audit` for details.
2,切换镜像源。
# 查看可用的镜像源
F:\web前端\vue\my-project>nrm ls
npm ---------- https://registry.npmjs.org/
yarn --------- https://registry.yarnpkg.com/
tencent ------ https://mirrors.cloud.tencent.com/npm/
cnpm --------- https://r.cnpmjs.org/
taobao ------- https://registry.npmmirror.com/
npmMirror ---- https://skimdb.npmjs.com/registry/
# 使用 taobao 镜像
F:\web前端\vue\my-project>nrm use taobao
Registry has been set to: https://registry.npmmirror.com/
最后放几篇对 Ryan Dahl 的采访:
node.js 是 JavaScript 运行环境和库的集合,并不参与项目的直接的创建等构建性的工作。
如果在项目中要用到第三方的包,实现包括安装、更新、删除等操作,我们可以使用 node.js 默认提供的包管理器 npm,它会通过依赖关系自动管理包。
在实际项目的模块化编程中,开发者通过抽象与封装,将程序分解成一个个模块,从而降低代码耦合度、提高可重用性,进而方便校验、测试。应用程序中每个模块都具有条理清楚的设计和明确的目的。
根据 webpack 官网的介绍,webpack 就是一款 JavaScript 应用程序的静态模块打包器(module bundler)。当用 webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 包:
新手可以先浏览 webpack :概念了解一些核心概念,然后再跟着 webpack :起步走一个流程,来看看如何使用 webpack。
脚手架就是构建项目的工具,使用脚手架能组织项目代码,形成符合项目规范的目录与文件结构。
Vue 项目的官方脚手架是 Vue CLI,它内部封装了 webpack,致力于将 Vue 生态中的工具基础标准化。它确保了各种构建工具能够基于智能的默认配置即可平稳衔接,这样你可以专注在撰写应用上,而不必花好几天去纠结配置的问题。
前端模块化开发中webpack、npm、node、nodejs之间的关系[小白总结]
前端开发3年了,竟然不知道什么是 Vue 脚手架?(下)
npm,node,webpack关系
回到教程,由于我们一直以来使用的都是 Vue 3 的语法,自然创建的就是 Vue 3 的项目。
先卸载 Vue 2 版本的脚手架。
# vue 2 脚手架对应于 vue-cli
F:\web前端\vue\mytest>npm uninstall vue-cli -g
up to date, audited 1 package in 178ms
found 0 vulnerabilities
然后安装 Vue 3 版本的脚手架。
# vue 3 脚手架对应于 @vue/cli
F:\web前端\vue\mytest>npm install -g @vue/cli
Vue CLI v5.0.4
? Please pick a preset:
> Default ([Vue 3] babel, eslint)
Default ([Vue 2] babel, eslint)
Manually select features
npm WARN deprecated [email protected]: See https://github.com/lydell/source-map-url#deprecated
npm WARN deprecated [email protected]: Please see https://github.com/lydell/urix#deprecated
npm WARN deprecated [email protected]: The `apollo-tracing` package is no longer part of Apollo Server 3. See https://www.apollographql.com/docs/apollo-server/migration/#tracing for details
npm WARN deprecated [email protected]: See https://github.com/lydell/source-map-resolve#deprecated
npm WARN deprecated [email protected]: The `graphql-extensions` API has been removed from Apollo Server 3. Use the plugin API instead: https://www.apollographql.com/docs/apollo-server/integrations/plugins/
npm WARN deprecated [email protected]: https://github.com/lydell/resolve-url#deprecated
npm WARN deprecated [email protected]: The functionality provided by the `apollo-cache-control` package is built in to `apollo-server-core` starting with Apollo Server 3. See https://www.apollographql.com/docs/apollo-server/migration/#cachecontrol for details.
npm WARN deprecated [email protected]: Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.
npm WARN deprecated [email protected]: The `subscriptions-transport-ws` package is no longer maintained. We recommend you use `graphql-ws` instead. For help migrating Apollo software to `graphql-ws`, see https://www.apollographql.com/docs/apollo-server/data/subscriptions/#switching-from-subscriptions-transport-ws For general help using `graphql-ws`, see https://github.com/enisdenjo/graphql-ws/blob/master/README.md
npm WARN deprecated [email protected]: This package has been deprecated and now it only exports makeExecutableSchema.\nAnd it will no longer receive updates.\nWe recommend you to migrate to scoped packages such as @graphql-tools/schema, @graphql-tools/utils and etc.\nCheck out https://www.graphql-tools.com to learn what package you should use instead
changed 895 packages in 51s
# 查看脚手架版本
F:\web前端\vue\mytest>vue -V
@vue/cli 5.0.4
创建 vue 项目。
F:\web前端\vue\mytest>vue create demo
Vue CLI v5.0.4
? Please pick a preset: Default ([Vue 3] babel, eslint)
Vue CLI v5.0.4
✨ Creating project in F:\web前端\vue\mytest\demo.
� Initializing git repository...
⚙️ Installing CLI plugins. This might take a while...
added 842 packages in 46s
� Invoking generators...
� Installing additional dependencies...
added 100 packages in 6s
⚓ Running completion hooks...
� Generating README.md...
� Successfully created project demo.
� Get started with the following commands:
$ cd demo
$ npm run serve
启动项目。
F:\web前端\vue\mytest>cd demo
F:\web前端\vue\mytest\demo>npm run serve
> [email protected] serve
> vue-cli-service serve
INFO Starting development server...
DONE Compiled successfully in 4725ms 9:29:54 ├F10: PM┤
App running at:
- Local: http://localhost:8081/
- Network: http://192.168.1.11:8081/
Note that the development build is not optimized.
To create a production build, run npm run build.
访问 http://localhost:8081/,效果如下:
连按两次 ctrl+c 可退出项目:
App running at:
- Local: http://localhost:8081/
- Network: http://192.168.1.11:8081/
Note that the development build is not optimized.
To create a production build, run npm run build.
终止批处理操作吗(Y/N)? Y
F:\web前端\vue\mytest\demo>
查看一下刚刚创建好的项目的结构:
这里对 Vue 3 默认的项目结构做一个简单的说明:
|-node_modules -- 存放项目所有的依赖包
|-public -- 存放存放第三方插件相关的静态资源
---|favicon.ico -- 网站的显示图标
---|index.html -- 是一个用于生成项目入口的模板文件,即浏览器访问项目时默认打开该文件
|-src -- 源文件目录,编写的代码基本都在这个目录下
---|assets -- 存放本地的静态资源,比如logo.png
---|components -- 存放自定义的公共组件
---|HelloWorld.vue -- 存放自定义的HelloWorld组件
---|App.vue -- 根组件,会在大规模项目中用于存放路由
---|main.js -- 程序入口文件,主要作用是初始化vue实例
|-.gitignore -- 配置gitignore
|-bable.config.js -- babel配置文件,用于转换es6语法
|-.browserslistrc -- 在不同前端工具之间公用目标浏览器和node版本的配置文件,作用是设置兼容性
|-jsconfig.json -- 配置js代码规范,用于规范js代码
|-package.json -- 配置项目的依赖包,用于规范项目依赖包
|-package-lock.json -- 配置项目依赖包的版本,用于规范项目依赖包的版本
|-README.md -- 项目的描述文件
|-vue.config.js -- 配置vue的相关配置
在前面,我们将所有东西都一股脑儿地放在了一个 html 文件中,在哪里定义根组件、在哪里注册子组件、在哪里创建应用并挂载等等操作都比较明显。
现在我们创建了一个简单的 Vue 项目,简单来说就是将原本的单 html 文件进行了切割,效果就是启动并访问指定的地址后,显示一个介绍页。但它内部是怎样的工作流程?
这就来简单说说项目结构中的各部分的组成内容以及它们是怎样配合工作的。
当运行项目启动命令后,会进入程序入口文件 main.js,那就先来看看 main.js 做了什么事情:
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')
其实是好理解的,就是使用 import 语句从根组件定义所在的 App.vue 文件中导入一个名为 App 的对象,并实例化。
这里就要说明一下,在工程化的 Vue 项目中,我们会尽量将提供某项功能的组件封装到一个单独的 .vue 文件中,这种文件就是组件文件。
现在就来看看根组件文件 App.vue 的内容:
<template>
<img alt="Vue logo" src="./assets/logo.png">
<HelloWorld msg="Welcome to Your Vue.js App"/>
template>
<script>
import HelloWorld from './components/HelloWorld.vue'
export default {
name: 'App',
components: {
HelloWorld
}
}
script>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
style>
一个组件文件通常包含三个部分:
先看模板代码:
<template>
<img alt="Vue logo" src="./assets/logo.png">
<HelloWorld msg="Welcome to Your Vue.js App"/>
template>
这个 HelloWorld 组件是在创建项目时自动产生的自定义组件,在这里它就是根组件的一个子组件。
这个子组件是怎样以哪种方式注册的?msg 有什么意义?
结渣看 js 逻辑代码:
<script>
import HelloWorld from './components/HelloWorld.vue'
export default {
name: 'App',
components: {
HelloWorld
}
}
</script>
export 语句用于对外提供本模块的接口,这里是一个注册过并指定了 name 的对象。
这两段代码共同作用的效果相当于:
<script>
import HelloWorld from './components/HelloWorld.vue'
export default {
name: 'App',
components: {
HelloWorld
},
template: `
`
}
script>
只不过在单文件组件中我们将它们分离了。
在进入 src/components/HelloWorld.vue 组件看看这个自定义的组件 HelloWorld :
<template>
<div class="hello">
<h1>{{ msg }}h1>
<p>
For a guide and recipes on how to configure / customize this project,<br>
check out the
<a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentationa>.
p>
<h3>Installed CLI Pluginsh3>
<ul>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel" target="_blank" rel="noopener">babela>li>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint" target="_blank" rel="noopener">eslinta>li>
ul>
<h3>Essential Linksh3>
<ul>
<li><a href="https://vuejs.org" target="_blank" rel="noopener">Core Docsa>li>
<li><a href="https://forum.vuejs.org" target="_blank" rel="noopener">Foruma>li>
<li><a href="https://chat.vuejs.org" target="_blank" rel="noopener">Community Chata>li>
<li><a href="https://twitter.com/vuejs" target="_blank" rel="noopener">Twittera>li>
<li><a href="https://news.vuejs.org" target="_blank" rel="noopener">Newsa>li>
ul>
<h3>Ecosystemh3>
<ul>
<li><a href="https://router.vuejs.org" target="_blank" rel="noopener">vue-routera>li>
<li><a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuexa>li>
<li><a href="https://github.com/vuejs/vue-devtools#vue-devtools" target="_blank" rel="noopener">vue-devtoolsa>li>
<li><a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener">vue-loadera>li>
<li><a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">awesome-vuea>li>
ul>
div>
template>
<script>
export default {
name: 'HelloWorld',
props: {
msg: String
}
}
script>
<style scoped>
h3 {
margin: 40px 0 0;
}
ul {
list-style-type: none;
padding: 0;
}
li {
display: inline-block;
margin: 0 10px;
}
a {
color: #42b983;
}
style>
仍然包含三个部分。
模板文件中的 {{ msg }} 表明 msg 是一个插值,显然它来自于后面 props
中的 msg,进一步表明,它是可以从父组件接收数据的。
还有,此处的 元素有一个
scoped
属性,表明要让此样式私有化。
好了,现在来捋一捋关系。
就这样吧,其他的东西我们也还没接触,就不说了。
现在就来将我们前面的一个例子以工程化的形式重构一下。
重构前:
<h2> Welcome to XXX's BLOGh2>
<div id="app">
<div id="dynamic-component">
<button
v-for="tab in tabs"
:key="tab"
:class="['tab-button', { active: currentTab === tab }]"
@click="currentTab = tab"
>
{{ tab }}
button>
<component v-bind:is="currentTabComponent" class="tab">component>
div>
<person-info :info="info">USER:person-info>
<div>
<custom-input v-model="searchText">custom-input>
<br>
<span v-if="searchText">you want to search {{ searchText }}span>
div>
<div :style="{ fontSize: postFontSize + 'em' }"
>
<posts-info v-for="post in posts"
:title="post.title"
:hot="post.hot"
:likes="post.likes"
:tags="post.tags"
:style="styleObject"
@like="likePost(post)"
>posts-info>
div>
div>
<script>
// 1,创建应用
const Root = {
data() {
return {
info: {
name: 'xiaolu2333',
},
posts: [
{id: 1, title: 'My journey with Vue', likes: 23, hot: false, tags: ['vue', 'vue.js', 'vue3']},
{id: 2, title: 'Blogging with Vue', likes: 14, hot: false, tags: ['vue.js', 'vue3']},
{id: 3, title: 'Why Vue is so interesting', likes: 100, hot: true, tags: ['vue', 'vue3']}
],
styleObject: {
title: 'color: green',
info: 'color: blue',
lineBreak: 'white-space:nowrap'
},
postFontSize: 1,
searchText: '',
currentTab: 'Home',
tabs: ['Home', 'Posts', 'Archive']
}
},
methods: {
likePost(post) {
post.likes += 1
}
},
computed: {
currentTabComponent() {
return 'tab-' + this.currentTab.toLowerCase()
}
}
}
// 2,创建应用
const app = Vue.createApp(Root)
// 3,定义一个名为 person-info 的新全局组件
app.component('person-info', {
props: ['info'],
template: `
{{ info.name }}
`
});
// 3,定义一个名为 posts-info 的新全局组件
app.component('posts-info', {
props: ['title', 'likes', 'hot', 'tags', 'style'],
template: `
{{ title }}
HOT
: {{ tag + " " }}
`
})
// 在组件上使用 v-model 自定义输入框
app.component('custom-input', {
props: ['modelValue'],
emits: ['update:modelValue'],
template: `
`
})
app.component('tab-home', {
template: `Home component`
})
app.component('tab-posts', {
template: `Posts component`
})
app.component('tab-archive', {
template: `Archive component`
})
// 4,挂载应用实例到 DOM,创建根组件实例
const vm = app.mount('#app')
script>
首先看看程序入口文件有什么需要改动的不?好像没有,ok,继续。
然后看看根组件 App.vue。这里首先修改模板代码:
<template>
<img alt="Vue logo" src="./assets/logo.png">
<h2> Welcome to XXX's BLOGh2>
<div id="dynamic-component">
<button
v-for="tab in tabs"
:key="tab"
:class="['tab-button', { active: currentTab === tab }]"
@click="currentTab = tab"
>
{{ tab }}
button>
<component v-bind:is="currentTabComponent" class="tab">component>
div>
<person-info :info="info">USER:person-info>
<div>
<custom-input v-model="searchText">custom-input>
<br>
<span v-if="searchText">you want to search {{ searchText }}span>
div>
<div :style="{ fontSize: postFontSize + 'em' }"
>
<posts-info v-for="post in posts"
:title="post.title"
:hot="post.hot"
:likes="post.likes"
:tags="post.tags"
:style="styleObject"
@like="likePost(post)"
>posts-info>
div>
template>
再修改根组件中 js 逻辑代码:
<script>
import PostsInfo from './components/PostsInfo.vue'
import PersonInfo from './components/PersonInfo.vue'
import CustomInput from './components/CustomInput.vue'
export default {
name: 'App',
data() {
return {
info: {
name: 'xiaolu2333',
},
posts: [
{id: 1, title: 'My journey with Vue', likes: 23, hot: false, tags: ['vue', 'vue.js', 'vue3']},
{id: 2, title: 'Blogging with Vue', likes: 14, hot: false, tags: ['vue.js', 'vue3']},
{id: 3, title: 'Why Vue is so interesting', likes: 100, hot: true, tags: ['vue', 'vue3']}
],
styleObject: {
title: 'color: green',
info: 'color: blue',
lineBreak: 'white-space:nowrap'
},
postFontSize: 1,
searchText: '',
currentTab: 'Home',
tabs: ['Home', 'Posts', 'Archive']
}
},
methods: {
likePost(post) {
post.likes += 1
}
},
computed: {
currentTabComponent() {
return 'tab-' + this.currentTab.toLowerCase()
}
},
components: {
PersonInfo,
PostsInfo,
CustomInput,
TabHome,
TabPosts,
TabArchive,
}
}
script>
暂时先不要样式样式。
然后在 src/components/ 目录下创建对应的六个单文件组件:
<template>
<h3>
<slot>slot>
{{ info.name }}
h3>
template>
<script>
export default {
name: 'PersonInfo',
props: {
info: String
}
}
script>
<template>
<div>
<h3 :style="style.title">{{ title }}h3>
<div :style="style.info">
<span v-if="hot">HOT span>
<button @click="$emit('like')">{{ likes }} button>
<span v-for="(tag,key) in tags" :key="key"><a>{{ tag + " " }} a>span>
div>
div>
template>
<script>
export default {
name: 'PostsInfo',
props: ['title', 'likes', 'hot', 'tags', 'style']
}
script>
<template>
<input
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)">
<button>searchbutton>
template>
<script>
export default {
name: 'CustomInput',
props: ['modelValue'],
emits: ['update:modelValue'],
}
script>