本文只讲不使用vue-cli
徒手搭建vue3
项目,目的是了解日常开发常用模块搭配,了解各个模块的功能作用,以便自己可以搭建除vue
之外的(如rect
或原生开发的)项目。实际开发时,如果使用到vue
或rect
,建议使用官方cli
创建项目,然后再安装其他常用模块会方便很多。
目标
- 打包压缩
- 热更新
- 编译ES6+使兼容主流浏览器
- 安装vue
- 支持编译scss
- css分离打包
- 固定模块单独打包
- css3兼容处理
- 响应式单位处理
- 静态资源处理
- 接口代理
- 多页面开发
一、实现打包压缩
- 运行
npm init -y
,此时在目录下生成了package.json
文件 - 运行
npm install -D webpack webpack-cli
安装webpack
,版本如下:
"webpack": "^5.39.0",
"webpack-cli": "^4.7.2"
- 在项目目录下创建目录
src
,并在src
内创建index.js
文件
demo
|-- node_modules
|-- src
|-- index.js
|-- package.json
- 在
index.js
随便写一些代码
async function fn(){
let n = await new Promise((r => {
setTimeout(() => {
r(1)
}, 1000);
}))
return n
}
fn().then(n => {
console.log(n)
})
- 在
package.json
文件内添加dev
和build
指令:
"scripts": {
"dev": "webpack --mode development",
"build": "webpack --mode production"
}
此时,运行npm run dev
或npm run build
即可正常打包,如运行npm run build
,会生成打包压缩后的文件dist/main.js
,其代码如下:
(async function(){return await new Promise((e=>{setTimeout((()=>{e(1)}),1e3)}))})().then((e=>{console.log(e)}));
可以看到代码已被打包压缩,因webpack4+
开始,内部使用了terser
压缩工具(据说uglify-js
不支持ES6+
)。
PS:可以通过配置webpack.config.js
如下关闭压缩
module.exports = {
optimization: false
}
当然,大多数时候这是不必要的,但也会有它的使用场景,比如使用webpack开发小程序,由于development
模式webpack会使用到eval
,而小程序不支持eval
,而开发时又不希望编译太慢(压缩极影响编译速度),此时就可以使用production
模式然后关掉optimization
。
二、实现热更新
- 运行
npm i -D webpack-dev-server html-webpack-plugin
,版本如下:
"html-webpack-plugin": "^5.3.1",
"webpack-dev-server": "^3.11.2"
- 在
package.json
添加start
指令:
"scripts": {
"start": "webpack serve --mode development --open",
"dev": "webpack --mode development",
"build": "webpack --mode production"
}
- 在项目目录下添加
webpack.config.js
文件,代码如下:
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
target: 'web',
devServer: {
contentBase: './dist',
},
plugins: [
new HtmlWebpackPlugin({
title: 'Demo'
}),
]
}
此时,运行npm start
,自动打开浏览器并打开地址http://localhost:8080
。
三、实现编译ES6+
从上方打包后的代码看到,并没有编译ES6+为ES5,所以需要使用babel
来编译。
- 运行
npm i -D @babel/core @babel/preset-env babel-loader
,版本如下:
"@babel/core": "^7.14.6",
"@babel/preset-env": "^7.14.5",
"babel-loader": "^8.2.2",
- 在
webpack.config.js
中添加
module: {
...
rules: [{
test: /\.m?js$/,
exclude: /(node_modules|bower_components)/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
}],
...
}
此时,运行npm run build
后,打包的代码虽然把let const async/await
等语法转换了,但还不完全是ES5
代码,比如Promise
对象。所以,接下来就来解决polyfill
的问题。
- 运行
npm i --save core-js
,它就是把polyfill
拆分成小颗粒的包代码库,以便babel-loader
动态的import
使用到的对象。版本如下:
"core-js": "^3.14.0"
- 在
package.json
添加字段browserslist
关于browserslist
"browserslist": [
"> 1%",
"last 2 versions",
"not dead",
"Android >= 4.4",
"iOS >=5"
]
这是移动端兼容目标浏览器的常用配置,不考虑IE
浏览器。
- 在
webpack.config.js
中修改babel-loader
的配置如下:
{
test: /\.m?js$/,
exclude: /(node_modules|bower_components)/,
use: {
loader: 'babel-loader',
options: {
// 编译必须排除core-js中的代码,不然可能会发生错误
exclude: [
/node_modules[\\\/]core-js/,
/node_modules[\\\/]webpack[\\\/]buildin/,
],
presets: [['@babel/preset-env', {
useBuiltIns: 'usage',
corejs: 3
}]]
}
}
}
此时运行npm run build
就可以看到控制台动态的打包了兼容处理的代码库
PS: 如果觉得动态import
不靠谱,那么也可以选择简单粗暴直接安装babel-polyfill
,然后直接import 'babel-polyfill'
- 运行
npm i --save whatwg-fetch
安装whatwg-fetch
,这是对web
端的fetch
的兼容处理,使能够在不同浏览器使用fetch
发送异步请求。这种单颗粒的直接在文件开头import 'whatwg-fetch'
即可。
四、安装vue 3.x
- 运行
npm i --save vue@next
安装vue
- 运行
npm i -D vue-loader@next @vue/compiler-sfc
,让webpack
支持单文件组件(sfc
) - 版本号如下:
"@vue/compiler-sfc": "^3.1.1",
"vue-loader": "^16.2.0",
"vue": "^3.1.1",
- 在
webpack.config.js
配置VueLoaderPlugin
和vue-loader
。另外,规范一下文件名和目录,把src
目录下的index.js
文件更名为main.js
,在项目目录下新增目录public
并添加index.html
文件,修改wepback.config.js
的对应配置。
目前为止,webpack.config.js
完整配置如下:
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { VueLoaderPlugin } = require('vue-loader');
module.exports = {
target: 'web',
entry: {
app: './src/main.js'
},
devServer: {
contentBase: './dist',
},
plugins: [
new HtmlWebpackPlugin({
title: 'Demo',
template: './public/index.html'
}),
new VueLoaderPlugin()
],
module: {
rules: [{
test: /\.m?js$/,
exclude: /(node_modules|bower_components)/,
use: {
loader: 'babel-loader',
options: {
exclude: [
/node_modules[\\\/]core-js/,
/node_modules[\\\/]webpack[\\\/]buildin/,
],
presets: [['@babel/preset-env', {
useBuiltIns: 'usage',
corejs: 3
}]]
}
}
},{
test: /\.vue$/,
loader: 'vue-loader'
}]
}
}
public/index.html
文件代码如下:
Demo
- 在
src
目录创建App.vue
文件,代码如下:
Hello Webpack!
- 将
src/main.js
的代码修改如下:
import {createApp} from 'vue'
import App from './App.vue'
createApp(App).mount('#app')
到此,vue3基本开发支持已正常完成,可以运行npm start
看看。
五、使用支持编译scss
- 运行
npm i -D style-loader css-loader sass-loader sass
,并在webpack.config.js
的module.rules
字段添加如下配置:
},{
test: /\.s[ac]ss$/i,
use: ['style-loader', 'css-loader', 'sass-loader']
}]
- 在
src/App.vue
文件修改style
标签并写简单的样式如下
运行npm start
不出意外的话,应该看到文字变红了。
六、分离css
目前配置运行npm run build
打包,js
和css
是完全揉在一起打包进app.js
文件的,我们希望把css
代码从app.js
中分离为单独的css
文件。
- 运行
npm i -D mini-css-extract-plugin
安装插件,版本如下:
"mini-css-extract-plugin": "^1.6.0",
- 修改
webpack.config.js
,由于在development
模式下,为了快速编译,故不应该分离css
,而应该只在production
模式使用这个插件。顺便,对不同开发模式做不同的其他配置项修改。完整配置如:
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { VueLoaderPlugin } = require('vue-loader')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const isProd = process.env.NODE_ENV === 'production'
const prodPlugins = []
if(isProd){
prodPlugins.push(new MiniCssExtractPlugin({
filename: '[name].[contenthash].css'
}))
}
module.exports = {
target: 'web',
mode: process.env.NODE_ENV,
entry: {
app: './src/main.js'
},
output: {
clean: isProd,
filename: isProd ? '[name].[contenthash].js' : '[name].js'
},
devServer: {
contentBase: './dist',
},
plugins: [
new HtmlWebpackPlugin({
title: 'Demo',
template: './public/index.html'
}),
new VueLoaderPlugin(),
...prodPlugins
],
module: {
rules: [{
test: /\.m?js$/,
exclude: /(node_modules|bower_components)/,
use: {
loader: 'babel-loader',
options: {
exclude: [
/node_modules[\\\/]core-js/,
/node_modules[\\\/]webpack[\\\/]buildin/,
],
presets: [['@babel/preset-env', {
useBuiltIns: 'usage',
corejs: 3
}]]
}
}
},{
test: /\.vue$/,
loader: 'vue-loader'
},{
test: /\.s[ac]ss$/i,
use: [isProd ? MiniCssExtractPlugin.loader : 'style-loader', 'css-loader', 'sass-loader']
},
]
}
}
上方代码中,process.env.NODE_ENV
目前是undefined
的,我们需要修改package.json
的指令,在执行环境中加入NODE_ENV
变量。这里需要安装cross-env
来处理跨平台兼容。
- 运行
npm i -D cross-env
,安装版本如下:
"cross-env": "^7.0.3",
- 修改
package.json
指令如下:
"scripts": {
"start": "cross-env NODE_ENV=development webpack serve --open",
"dev": "cross-env NODE_ENV=development webpack",
"build": "cross-env NODE_ENV=production webpack"
},
这样,当运行npm run build
时,就可以看到css
被分离了。
七、固定模块单独打包
因为像vue
的源码部分是固定不变的,应该把它与页面的逻辑代码分离出去,这样在项目版本跌代时,vue
就可以来自缓存,而只需要加载逻辑代码。
- 在
webpack.config.js
添加如下配置:
optimization: {
splitChunks: {
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendor',
chunks: 'all',
},
},
},
},
意思是把所有的来自node_modules
模块都打包到vendor.js
中。
八、css3兼容处理
- 运行
npm i -D postcss-loader postcss postcss-preset-env
,版本如下:
"postcss": "^8.3.5",
"postcss-loader": "^6.1.0",
"postcss-preset-env": "^6.7.0",
- 修改
webpack.config.js
中的module.rules
里的css-loader
后加入如下:
{
test: /\.s[ac]ss$/i,
use: [
isProd ? MiniCssExtractPlugin.loader : 'style-loader',
'css-loader',
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: [
'postcss-preset-env'
],
},
},
},
'sass-loader'
]
}
- 在
App.vue
中添加css3
代码,然后运行npm start
测试看看
可以看到
animation
被自动补上了-webkit-
。
PS: postcss-preset-env
已经包含了autoprefixer
,所以不用再安装autoprefixer
。postcss-preset-env
会根据package.json
的browserslist
字段来处理对应的兼容性。
九、响应式单位处理
一般有两种,rem
和vw
,这里由于是移动端项目,所以推荐vw
。设计稿的宽度标准是750px
。
- 运行
npm i -D postcss-plugin-pxtoviewport
,版本如下:
"postcss-plugin-pxtoviewport": "0.0.6",
- 在
webpack.config.js
的postcss-loader
加入插件
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: [
'postcss-preset-env',
['postcss-plugin-pxtoviewport', {
viewportWidth: 750, // 设计稿宽
unitPrecision: 3, // 计算结果不整除时要保留的小数位数
viewportUnit: 'vw', // 使用单位vw
minPixelValue: 1, // 小于这个值时不处理
mediaQuery: true // 允许在媒体查询中转换`px`
}]
],
},
},
},
一般移动端页面是根据页面宽而高度自动等比缩放,所以,只需要所有尺寸都基于这个宽度即可。
- 修改
App.vue
样式代码如下:
...
div{
background: green;
height: 150px;
}
...
可以看到,
150px
被转换成了20vw
,其计算公式100*(150/750)
,这样,就可以放心的按设计稿尺寸去写css
了。
十、静态资源处理
接下来要处理的是图片、音视频、字体等的文件。引入方式有两种,一种是有.
开头的路径(如'./'
和'../'
),需要url-loader file-loader raw-loader
等这类加载器,另一种是无.
开头的路径(如'/'
和''
),需要copy-webpack-plugin
拷贝,包含目录拷贝。
- 第一种,
webpack5
已经内置了资源加载模块,直接配置即可,无需安装loader
。在webpack.config.js
的module.rules
数组中添加以下配置。
rules: [
...
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
type: 'asset'
},{
test: /\.(mp4|webm|ogg|mp3|wav|flac|aac|woff2?|eot|ttf|otf)(\?.*)?$/,
type: 'asset/resource'
}]
type
说明:
asset
: 表示让webpack
决定选择data uri
(即base64)或uri
形式;
asset/resoure
: 表示使用uri
形式。
点击了解更多参数
- 第二种,运行
npm i -D copy-webpack-plugin
安装插件,然后在webpack.config.js
头入引入插件,然后在plugins
数组中添加如下:
const path = require('path')
const CopyWebpackPlugin = require('copy-webpack-plugin')
...
plugins:[
new CopyWebpackPlugin({
patterns: [{
from: path.resolve(__dirname, 'public'),
to: path.resolve(__dirname, 'dist'),
toType: 'dir',
filter: resourcePath => {
return !/\.html$/.test(resourcePath)
}
}]
})
]
上方配置是把public
目录下的资源文件拷贝到dist
,并且忽略掉.html
文件。更多目录可以继续往patterns
数组中添加。
- 运行
npm start
测试一下,把图片logo.png
放在public
中,然后在App.vue
中添加
看看,然后再改成
,此时会报错找不到资源,需要把logo.png
移动到src
目录。可自行测试不同大小的图片,看看各种情况生成的地址。
十一、接口代理
接口代理其实很简单,但很多不熟悉的人经常调试不通,因为没有真正理解,所以对不同的情况不知道怎么配置。
情况一:后端接口路径都是同一个开头,如:/api/login, /api/user, /api/xxx
。这就好办了,简单配置如下即可。
devServer: {
contentBase: './dist',
proxy: {
'/api': {
target: 'http://localhost:3000',
secure: true
}
}
},
在页面中发请求示例:
fetch('/api/login', {})
.then(res => res.json())
.then(res => {
console.log(res)
})
后端最终接收到的请求是来自地址http://localhost:3000/api/login
情况二:后端接口路径开头不同,如: /login, /user, /xxx
。这种有两种配置可以选择:
- 在页面中开发时将所有接口都以特定路径开头,部署正式环境时再去掉,比如同样使用
/api
开头,则配置如下:
devServer: {
contentBase: './dist',
proxy: {
'/api': {
target: 'http://localhost:3000',
secure: true,
pathRewrite: {
'^/api': ''
}
}
}
},
在页面中发请求示例:
fetch('/api/login', {}).then(res => res.json()).then(res => {
console.log(res)
})
后端最终接收到的请求是来自地址http://localhost:3000/login
,注意与情况一的区别,这里pathRewrite
的配置就是把页面发请求时带的/api
去掉,使后端真正得到的地址是http://localhost:3000/login
。
这种方式比较稳,不怕与页面路径有冲突,但有一个缺点就是发布正式时要把页面中的/api
去掉,因为到正式环境时接口和页面地址是同一个域下,不需要代理。一般处理方式是:
// api.js
const apiBase = /^http\:\/\/localhost/.test(window.location.href) ? '/api' : ''
export function request(path, params){
// TODO: build params
return fetch(apiBase + path, params).then(res => res.json())
}
这样导出一个request
函数来专门发送请求
import {request} from './api'
request('/login', {}).then(res => {
console.log(res)
})
这样,只要发布正式的地址不是http://localhost
访问就正常。
接下来说另一种配置
- 保持地址原样,正式环境与开发环境一致,无需在页面处理
devServer: {
contentBase: './dist',
proxy: [{
context: ['/login', '/user'],
target: 'http://localhost:3000',
secure: true
}]
},
在页面中发请求示例:
fetch('/login', {}).then(res => res.json()).then(res => {
console.log(res)
})
后端最终接收到的请求地址还是http://localhost:3000/login
,这种配置虽然保持了开发环境还正式环境相同的页面代码,但也有缺点,如果开头不一样接口很多,有十几个或几十个,那得在context
字段全部写上,如此,单面应用history
模式或多页面开发很容易冲突,比如有一个用户信息html
页面的访问地址是http://localhost:8080/user
,而获取用户信息的接口是/user
且是GET
请求,那么访问http://localhost:8080/user
时,不会进入html
页面,而是被代理到了http://localhost:3000/user
。所以,一般不推荐。
十二、多页面开发配置
- 先把目录结构改成如下:
两个index.js
的代码均如下:
import {createApp} from 'vue'
import App from './App.vue'
createApp(App).mount('#app')
src/index/App.vue
代码如:
index page
src/login/App.vue
代码如:
login page
- 修改
webpack.config.js
以下几个地方:
...
entry: {
index: './src/index/index.js',
login: './src/login/index.js'
},
...
plugins: [
new HtmlWebpackPlugin({
title: 'index',
template: './public/index.html',
filename: 'index.html',
chunks: ['index']
}),
new HtmlWebpackPlugin({
title: 'login',
template: './public/index.html',
filename: 'login.html',
chunks: ['login']
}),
...
]
运行npm start
就可看到页面index page
,在浏览器输入地址http://localhost:8080/login.html
就可以访问login
页面。
这就是最基本的多页面配置。