从官网下载nodejs
下载适合你电脑的对应版本,选择老版本的node可以防止出现兼容问题
安装Node.js
node -v
npm也安装好了
npm -v
创建文件夹
mkdir project
进入
cd project
npm初始化
npm init
新建public文件夹index.html,新建 src/index.js
<body>
<div id="app"></div>
</body>
下载 webpack 核心库,webpack 指令库
npm i webpack webpack-cli
新建webpack.config.js文件
webpack只可以解析js,要解析其它文件就要下载加载器
先下载html webpack 插件
npm i html-webpack-plugin
下载开发服务器
npm i webpack-dev-server
webpack.config.js:
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
mode: 'development',//开发模式
entry: './src/index.js', //入口文件
devServer: { // 开发服务器
port: 8080
},
plugins: [ //插件
new HtmlWebpackPlugin({
template: path.join(__dirname, './public/index.html')
//以哪个html文件作为模板,这将是 spa
}),
]
}
在package.json配置开发环境命令
"scripts": {
"dev": "webpack serve --config ./webpack.config.js"
},
运行:
npm run dev
打开 localhost:8080 就可以看到项目
接下来使用 vue 文件
下载vue:
npm i vue
src 创建 App.vue
<template>
<div class="a">{{ msg }}</div>
</template>
<script>
export default {
name: 'app',
data() {
return {
msg: 'App.vue'
}
}
}
</script>
index.js(vue3的写法):
import { createApp } from 'vue';
import App from './App.vue'
const app = createApp(App);
app.mount('#app');
下载 vue-loader
npm i vue-loader
引入:
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { VueLoaderPlugin } = require('vue-loader')
module.exports = {
mode: 'development',//开发模式
entry: './src/index.js', //入口文件
devServer: { // 开发服务器
port: 8080
},
plugins: [ //插件
new HtmlWebpackPlugin({
template: path.join(__dirname, './public/index.html')
//以哪个html文件作为模板,这将是 spa
}),
new VueLoaderPlugin()
],
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader'
},
]
}
}
加载css、scss文件:
下载:
npm i style-loader css-loader sass sass-loader
配置规则:
webpack.config.js:
module.exports = {
module: {
rules: [
{
test: /\.css$/i,
use: [
'style-loader',
'css-loader'
]
},
{
test: /\.scss$/i,
use: [
'style-loader',
'css-loader',
'sass-loader'
]
},
]
}
}
注意顺序:sass-loader会将scss代码转化为css代码,css-loader处理之后会导出样式数组,style-loader 是通过一个JS脚本创建一个style标签,里面包含一些样式。
App.vue:
<template>
<div class="a">{{ msg }}</div>
</template>
<script>
export default {
name: 'app',
data() {
return {
msg: 'App.vue'
}
}
}
</script>
<style scoped>
.a {
color: red;
background-color: green;
}
</style>
<style lang="scss">
.a {
width: 300px;
height: 300px;
}
</style>
新建 src/style.css:
.a {
font-size: 40px;
}
src/style.scss:
body {
#app {
.a {
font-weight: bold;
}
}
}
index.js:
import './style.css'
import './style.scss'
import { createApp } from 'vue';
import App from './App.vue'
const app = createApp(App);
app.mount('#app');
这样,所有样式都是正常的:
在style标签上加scoped属性,实现样式在当前组件生效:
<style scoped>
.a {
color: red;
background-color: green;
}
</style>
经过 vue-loader 处理之后会变成这样:
.a[data-v-7ba5bd90] {
color: red;
background-color: green;
}
vue-loader 处理过后就到了 css-loader,之后就到 style-loader
css-loader 处理过后会被 vue-style-loader 引用,方便在 vue 文件中创建 style 标签把样式给到 dom,vue-style-loader 还支持服务端渲染,如果你的 vue 项目是需要支持服务端渲染的,那么就需要 vue-style-loader 了
npm i vue-style-loader
引入:
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader'
},
{
test: /\.css$/i,
use: [
'style-loader',
'vue-style-loader',
'css-loader'
]
},
{
test: /\.scss$/i,
use: [
'style-loader',
'vue-style-loader',
'css-loader',
'sass-loader'
]
},
]
}
使用 axios 发送请求
npm i axios
新建 /src/api/index.js ,配置基础地址、请求超时、请求拦截、响应拦截
使用 element-ui 提供的消息提示,这里是 vue3,使用element-plus
npm i element-plus
全局注册 element-plus:
import './style.css'
import './style.scss'
import { createApp } from 'vue';
import App from './App.vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
const app = createApp(App);
// 中文化
app.use(ElementPlus, {
locale: zhCn,
})
// 注册 icon 组件
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
app.mount('#app');
/src/api/index.js :
import axios from 'axios'
import { ElMessage } from 'element-plus'
const server = axios.create({
// 配置一个公共的前置链接
baseURL: 'http://127.0.0.1:3030',
// 请求超时
timeout: 5000
})
// 请求拦截
server.interceptors.request.use((config) => {
config.headers.Authorization = localStorage.getItem('token') || ''
return config
}, (error) => {
return Promise.reject(error)
})
// 响应拦截
server.interceptors.response.use((response) => {
return response
}, (error) => {
if (error.response?.config.url === '/authentication') return error.response
if (error.response?.status === 401) {
ElMessage.warning('没有权限,请重新登录')
setTimeout(() => {
window.location.href = '/login'
}, 1800)
return error.response
}
if (error.response?.status === 404) {
ElMessage.warning('没有该接口')
return error.response
}
return Promise.reject(error)
})
export default server
新建 /src/api/users.js
import server from '.'
export const getUserList = () => server({
url: '/users',
method: 'get',
});
引入 getUserList :
App.vue:
<script>
import { getUserList } from '@/api/users.js'
export default {
name: 'app',
data() {
return {
msg: 'App.vue',
data: ''
}
},
methods: {
async getData() {
const res = await getUserList()
console.log(res)
}
}
}
</script>
配置绝对路径:
module.exports = {
resolve: {
alias: {
'@': path.join(__dirname, 'src')
},
}
}
当然,需要使用 babel-loader 去处理 js 文件:
npm i babel-loader
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader'
},
{
test: /\.css$/i,
use: [
'style-loader',
'vue-style-loader',
'css-loader'
]
},
{
test: /\.scss$/i,
use: [
'style-loader',
'vue-style-loader',
'css-loader',
'sass-loader'
]
},
{
test: /\.js$/,
loader: 'babel-loader'
},
]
}
配置 devTool 可以更细颗粒的反应开发环境遇到的错误和警告
module.exports = {
devtool: 'eval-cheap-module-source-map',
}
可以正常获取到数据
App.vue:
<template>
<div>
<div class="a">{{ msg }}</div>
<div v-for="item in data"><span>{{ item.uname }}</span> | <span>{{ item.nickname }}</span></div>
<el-button type="success" @click="getData">获取数据</el-button>
</div>
</template>
<script>
import { getUserList } from '@/api/users.js'
export default {
name: 'app',
data() {
return {
msg: 'App.vue',
data: []
}
},
methods: {
async getData() {
const res = await getUserList()
this.data = res.data.data
}
}
}
</script>
<style scoped>
.a {
color: red;
background-color: green;
}
</style>
<style lang="scss">
.a {
width: 300px;
height: 300px;
}
</style>
引入图片,新建 /src/assets 目录,放入logo.png
<img src="@/assets/logo.png" alt="">
处理图片:
module.exports = {
module: {
rules: [
{
test: /\.(png|jpg|jpeg|gif|svg)$/i,
type: 'asset',
}
]
},
}
使用 postcss-px-to-viewport来实现移动端的适配
安装:
npm i postcss postcss-loader postcss-px-to-viewport
在项目根目录新建一个 postcss.config.js :
module.exports = {
plugins: {
'postcss-px-to-viewport': {
unitToConvert: 'px', // 要转换的单位
viewportWidth: 750, // 以什么视图大小作为参照(UI设计稿的实际宽度)
unitPrecision: 5, // 保留的小数位数
propList: ['*'], // 指定哪些属性需要转换
viewportUnit: 'vw', // 转换的单位 目标
fontViewportUnit: 'vw', // 字体的转换单位
selectorBlackList: [], // 哪些选择器不做转换
minPixelValue: 1, // 最小的转换值(大于等于1才开始转换)
mediaQuery: false, // 是否支持 媒体查询
replace: true,
exclude: [/^node_modules$/], // 判断哪些目录下的的文件不转换
landscape: false,
landscapeUnit: 'vw',
landscapeWidth: 568
}
}
}
修改 webpack.config.js :
module.exports = {
module: {
rules: [
{
test: /\.css$/i,
use: [
'style-loader',
'vue-style-loader',
'css-loader',
'postcss-loader',
]
},
{
test: /\.s[ca]ss$/i,
use: [
'style-loader',
'vue-style-loader',
'css-loader',
'postcss-loader',
'sass-loader'
]
},
]
},
}
经过以上的操作之后,我们写出的页面使用的是px单位,但是,插件会帮我们自动的转换成vw单位。从而能实现移动端的适配。如果是移动端的开发就需要使用这个配置。
配置 typescript 环境
安装 ts-loader:
https://github.com/TypeStrong/ts-loader
npm i ts-loader typescript
引入:
module: {
rules: [
{
test: /\.(ts|tsx)$/,
loader: "ts-loader",
},
]
},
当然,在 vue 文件中 也是ts
<script lang="ts">
</script>
module: {
rules: [
{
test: /\.(ts|tsx)$/,
loader: "ts-loader",
options: { appendTsSuffixTo: [/\.vue$/] }
},
]
},
module.exports = {
resolve: {
// 添加“.ts”和“.tsx”作为可解析的扩展名
extensions: [".ts", ".tsx", ".js"],
// 添加对TypeScripts完全限定ESM导入的支持
extensionAlias: {
".js": [".js", ".ts"],
".cjs": [".cjs", ".cts"],
".mjs": [".mjs", ".mts"]
},
}
}
将所有 js 文件改为 ts 文件
/src/api/index.ts :
import axios, { AxiosRequestConfig, AxiosError, AxiosResponse } from 'axios'
import { ElMessage } from 'element-plus'
const server = axios.create({
// 配置一个公共的前置链接
baseURL: 'http://127.0.0.1:3030',
// 请求超时
timeout: 5000
})
// 请求拦截
server.interceptors.request.use((config: AxiosRequestConfig | any) => {
config.headers.Authorization = localStorage.getItem('token') || ''
return config
}, (error: AxiosError) => {
return Promise.reject(error)
})
// 响应拦截
server.interceptors.response.use((response: AxiosResponse) => {
return response
}, (error: AxiosError) => {
if (error.response?.config.url === '/authentication') return error.response
if (error.response?.status === 401) {
ElMessage.warning('没有权限,请重新登录')
setTimeout(() => {
window.location.href = '/login'
}, 1800)
return error.response
}
if (error.response?.status === 404) {
ElMessage.warning('没有该接口')
return error.response
}
return Promise.reject(error)
})
export default server
在根目录新建 tsconfig.json 文件:
{
"compilerOptions": {
"sourceMap": true,
"paths": {
"@/*": [
"./src/*"
]
},
"declaration": true,
},
}
安装 tsconfig-paths-webpack-plugin 插件解析 tsconfig.json:
tsconfig-paths-webpack-plugin - npm
npm i tsconfig-paths-webpack-plugin
修改 webpack.config.ts :
import TsconfigPathsPlugin from 'tsconfig-paths-webpack-plugin'
module.exports = {
resolve: {
plugins: [new TsconfigPathsPlugin({ configFile: "./tsconfig.json" })],
}
}
在 ts 环境中 *.vue 文件 sfc 要有 ts 类型校验,要在根目录新建一个 index.d.ts 文件,因为 webpack.config.ts 在根目录而且把 /src/index.ts 作为入口文件,而在 /src/index.ts 中,又以 /src/App.vue 作为 SPA 的根组件
index.d.ts:
// 非 ts 文件都需要声明模块,才可以被导入
declare module '*.vue' {
import type { DefineComponent } from 'vue';
const component: DefineComponent<any, any, any>;
export default component;
}
declare module 'element-plus/dist/locale/zh-cn.mjs'
使用 require 方式导入图片,App.vue:
<template>
<div>
<div class="a">{{ msg }}</div>
<img :src="logo" alt="" class="img">
<div v-for="item in data"><span>{{ item.uname }}</span> | <span>{{ item.nickname }}</span></div>
<el-button type="success" @click="getData">获取数据</el-button>
</div>
</template>
<script lang="ts">
import { getUserList } from '@/api/users'
export default {
name: 'app',
data() {
return {
logo: require('@/assets/logo.png'),
msg: 'App.vue',
data: []
}
},
methods: {
async getData() {
const res = await getUserList()
this.data = res.data.data
}
}
}
</script>
<style scoped lang="scss">
.a {
width: 300px;
height: 300px;
color: red;
background-color: green;
}
.img {
// background-image: url('@/assets/logo.png');
width: 200px;
height: 200px;
}
</style>
使用 vue-router 配置路由:
npm i vue-router
vue-router 通常使用这两种模式:
hash 模式,使用 URL 中的 # 作为路由,支持所有的浏览器。hash+pushState/hashChange 兼容性好,不刷新页面,因为服务端无法获取hash值,对 seo 优化不好,比如https://baidu.com/a 会直接显示未找到,而 https://baidu.com/#a 还是显示的是百度的首页,因为服务器获取不到# 后面的数据。
history 模式,浏览器提供 historyApi( go/back/forward ) 和新增的两个 pushState()/replaceState (),这两个api可以在不刷新的情况下读取浏览器的历史记录,history模式美观,但是需要服务端的支持,否则刷新会出现404状态。
新建 /src/router/index.ts,使用 histroy 模式,路由懒加载:
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
const routes: Array<RouteRecordRaw> = [
{
path: '/',
name: '/',
redirect: '/home'
},
{
path: '/home',
name: 'home',
component: import('@/views/Home.vue')
},
{
path: '/about',
name: 'about',
component: import('@/views/About.vue')
},
{
path: '/:pathMatch(.*)',
name: 'notFound',
component: import('@/views/NotFound.vue')
}
]
const router = createRouter({
// createWebHashHistory 带#,兼容IE
history: createWebHistory(), //不带#
routes
})
export default router
新建 /src/views/Home.vue、 /src/views/About.vue、 /src/views/NotFound.vue
全局使用 router :
index.ts:
import './style.css'
import './style.scss'
import { createApp } from 'vue';
import App from './App.vue'
import router from './router'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
const app = createApp(App);
app.use(router)
// 中文化
app.use(ElementPlus, {
locale: zhCn,
})
// 注册 icon 组件
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
app.mount('#app');
当使用 history 模式时,URL 会看起来很 "正常"。不过,问题来了,由于我们的应用是一个单页的客户端应用,如果没有适当的服务器配置,用户在浏览器中直接访问 http://localhost:8080/about 就会得到一个 404 错误,这就尴尬了。
不用担心:要解决这个问题,你需要做的就是在你的服务器上添加一个简单的回退路由。如果 URL 不匹配任何静态资源,它应提供与你的应用程序中的 index.html
相同的页面。
对于原生 Node.js 服务器:
const http = require('http')
const fs = require('fs')
const httpPort = 3030
http.createServer((req, res) => {
fs.readFile('index.html', 'utf-8', (err, content) => {
if (err) {
console.log('We cannot open "index.html" file.')
}
res.writeHead(200, {
'Content-Type': 'text/html; charset=utf-8',
})
res.end(content)
})
}).listen(httpPort, () => {
console.log('Server listening on: http://localhost:%s', httpPort)
})
配置好了之后,刷新不会出现404状态
App.vue:
<template>
<div>
<div>
<router-link :to="{ name: 'home' }">首页</router-link> |
<router-link :to="{ name: 'about' }">关于我们</router-link>
</div>
<router-view></router-view>
</div>
</template>
<script lang="ts">
export default {
name: 'app',
data() {
return {
}
}
}
</script>
引入 element-plus 的组件:
Table 表格 | Element Plus
/src/views/Home.vue :
<template>
<el-table ref="singleTableRef" :data="tableData" highlight-current-row style="width: 100%"
@current-change="handleCurrentChange">
<el-table-column type="index" width="50" />
<el-table-column property="date" label="Date" width="120" />
<el-table-column property="name" label="Name" width="120" />
<el-table-column property="address" label="Address" />
</el-table>
<div style="margin-top: 20px">
<el-button @click="setCurrent(tableData[1])">Select second row</el-button>
<el-button @click="setCurrent()">Clear selection</el-button>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import { ElTable } from 'element-plus'
interface User {
date: string
name: string
address: string
}
const currentRow = ref()
const singleTableRef = ref<InstanceType<typeof ElTable>>()
const setCurrent = (row?: User) => {
singleTableRef.value!.setCurrentRow(row)
}
const handleCurrentChange = (val: User | undefined) => {
currentRow.value = val
}
const tableData: User[] = [
{
date: '2016-05-03',
name: 'Tom',
address: 'No. 189, Grove St, Los Angeles',
},
{
date: '2016-05-02',
name: 'Tom',
address: 'No. 189, Grove St, Los Angeles',
},
{
date: '2016-05-04',
name: 'Tom',
address: 'No. 189, Grove St, Los Angeles',
},
{
date: '2016-05-01',
name: 'Tom',
address: 'No. 189, Grove St, Los Angeles',
},
]
</script>
渲染正常:
分离配置文件
我们项目当中需要根据不同的需求配置不同的配置文件,常见的情况就是分离成:
一个测试的配置文件,一个是打包生产的配置文件
在根目录新建一个 config 文件夹
在其下新建:webpack.common.ts 公共配置文件、webpack.dev.ts 测试环境配置文件、webpack.prod.ts 生成打包配置文件
我们可以通过 webpack-merge 来帮我们合并两个配置信息:
npm i webpack-merge
webpack.common.ts 公共配置文件,配置基础的loader和插件 :
import * as path from 'path'
import * as HtmlWebpackPlugin from 'html-webpack-plugin'
import { VueLoaderPlugin } from 'vue-loader'
import TsconfigPathsPlugin from 'tsconfig-paths-webpack-plugin'
import * as webpack from 'webpack'
export default {
mode: 'development',//开发模式
entry: './src/index.ts', //入口文件
devtool: "inline-source-map",
devServer: { // 开发服务器
port: 8080
},
plugins: [ //插件
new HtmlWebpackPlugin({
template: path.join(__dirname, '../public/index.html')
}),
new VueLoaderPlugin(),
new webpack.DefinePlugin({
'process.env.API_URL': JSON.stringify(process.env.API_URL)
})
],
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader'
},
{
test: /\.css$/i,
use: [
'style-loader',
'vue-style-loader',
'css-loader',
]
},
{
test: /\.s[ca]ss$/i,
use: [
'style-loader',
'vue-style-loader',
'css-loader',
'sass-loader'
]
},
{
test: /\.js$/,
loader: 'babel-loader'
},
{
test: /\.(ts|tsx)$/,
loader: "ts-loader",
options: { appendTsSuffixTo: [/\.vue$/] }
},
{
test: /\.(png|jpg|jpeg|gif|svg)$/i,
type: 'asset',
}
]
},
resolve: {
plugins: [new TsconfigPathsPlugin({ configFile: "./tsconfig.json" })],
// 添加“.ts”和“.tsx”作为可解析的扩展名
extensions: [".ts", ".tsx", ".js"],
// 添加对TypeScripts完全限定ESM导入的支持
extensionAlias: {
".js": [".js", ".ts"],
".cjs": [".cjs", ".cts"],
".mjs": [".mjs", ".mts"]
},
alias: {
'@': path.join(__dirname, '../src')
},
}
}
webpack.dev.ts 测试环境配置文件 :
import { merge } from 'webpack-merge'
import commonConfig from './webpack.common'
const devConfig: any = {
mode: 'development',
plugins: [
],
devServer: {
port: 8080, // 调试服务器的运行端口
open: true, // 是否自动打开浏览器
// 通常在开发环境下会出现跨域问题,在前端都是通过配置 proxy 解决,因为 jsonp 已经过时了
proxy: {
'/api': {
target: 'http://localhost:8081',
changeOrigin: true,
pathRewrite: {
'^/api': ''
}
}
},
},
}
module.exports = merge(commonConfig, devConfig)
webpack.prod.ts 生成打包配置文件 :
const { merge } = require('webpack-merge')
const commonConfig = require('./webpack.common')
const devConfig = {
mode: 'production',
plugins: [
]
}
module.exports = merge(commonConfig, devConfig)
修改 package.json 的启动和打包脚本:
"scripts": {
"dev": "webpack serve --config ./config/webpack.dev.ts",
"build": "webpack build --config ./config/webpack.prod.ts"
},
配置 开发环境和生产环境的接口地址,在根目录下新建
.env.dev :
API_URL = http://localhost:3030
.env.prod :
API_URL = http://www.abc.com
使用 dotenv-webpack 进行配置,一个安全的webpack插件,支持dotenv和其他环境变量,并且只公开您选择和使用的内容。
npm i dotenv-webpack
声明模块:
declare module 'dotenv-webpack'
修改 webpack.dev.ts :
import { merge } from 'webpack-merge'
import commonConfig from './webpack.common'
import * as DotEnv from 'dotenv-webpack'
const devConfig: any = {
mode: 'development',
plugins: [
new DotEnv({
path: './.env.dev',
safe: true
})
],
devServer: {
port: 8080, // 调试服务器的运行端口
open: true, // 是否自动打开浏览器
// 通常在开发环境下会出现跨域问题,在前端都是通过配置 proxy 解决,因为 jsonp 已经过时了
proxy: {
'/api': {
target: 'http://localhost:8081',
changeOrigin: true,
pathRewrite: {
'^/api': ''
}
}
},
},
}
module.exports = merge(commonConfig, devConfig)
修改 webpack.prod.ts :
import { merge } from 'webpack-merge'
import commonConfig from './webpack.common'
import * as DotEnv from 'dotenv-webpack'
const devConfig = {
mode: 'production',
plugins: [
new DotEnv({
path: './.env.prod',
safe: true
})
]
}
module.exports = merge(commonConfig, devConfig)
通过 process.env.API_URL 获取环境变量
配置好了开发的环境变量和生产的环境变量,打包的时候不需要修改代码
在webpack5当中,内置了一个 asset-module 模块, 方便我们在引入静态文件的时候进行打包或者是转换成base64
修改 webpack.common.ts :
export default {
module: {
rules: [
{
test: /\.(png|jpg|jpeg|gif|svg)$/i,
type: 'asset',
// 设置 type 为 asset 让 webpack 按照静态文件的方式来处理文件
generator: {
// 会给打包后的图片添加一串hash值
filename: 'static/imgs/[hash][ext]'
},
parser: {
dataUrlCondition: {
maxSize: 10 * 1024,
// 小于10kb的以上类型文件直接转换为 base64,否则单独生成一个文件
// 这样做,对于我们项目当中放多的小图片都不再打包成文
// 减少了打包的大小,并且减少了资源的链接消耗
}
}
}
]
},
}
mini-css-extract-plugin
npm i mini-css-extract-plugin
这个插件将CSS提取到单独的文件中。它为每个包含CSS的JS文件创建一个CSS文件。它支持CSS和SourceMaps的按需加载。
修改 webpack.common.ts :
import * as MiniCssExtractPlugin from 'mini-css-extract-plugin'
export default {
plugins: [
new MiniCssExtractPlugin(),
],
}
npm run build
开启持久化的存储缓存
export default {
cache: {
type: 'filesystem'// 缓存,提供二次打包速率
}
}
开启之后的第一次打包所需时间是正常的,从第二次打包开始,打包的速度会有大幅的提高