前言#
我工作不饱和(误),所以找点事做,俗称没事找事~
其实是早有预谋,选在这个时机是我怕以后没机会了
当前配置#
该项目是基于vue-cli创建的webpack-vue2的开发环境,package.json配置如下:
webpack4.x
vue2.6.14,有一个`local`,还有一个通过`script`引入的本地文件(可以简单看作是cdn的)
其它依赖,比如`babel`、`node-sass`等
公司自己的组件库,简称lib_a,仅dev环境
公司自己的组件库,简称lib_b,基于lib_a,也用于生产环境
部门自行封装的组件库,简称lib_c,script引入由于代码是公司的资产,所以这里简单的说下配置(vue.config.js + babel.config.js):
- 使用html-webpack-plugin,插入动态内容到模板里,比如开发环境引用的资源是
src,生产环境得是min - 配置
devServer代理部分接口 - 配置
css.loaderOptions.postcss的autoprefixer兼容css - 配置
css.less,覆盖组件库lib_b定义的主题色变量(lib_b是用less预处理样式) - 定义一些
alias - 由于项目中包含一些
ts文件,所以使用babel-loader + @babel/preset-typescript处理 - 配置
pages,多页面应用 - 使用
transpileDependencies帮忙处理一些自行封装的第三方库 - babel.config.js中配置@vue/cli-plugin-babel,和
vue-cli绑定的babel-preset版本
这里还是贴demo代码,避免大家仅看文字看不太明白:
vue.config.js:
const webpack = require('webpack');
const path = require('path');
const resolve = dir => path.resolve(__dirname, dir);
const isProd = process.env.NODE_ENV === 'production';
// ...
let vueConfig = {
// ...
devServer: {
disableHostCheck: true,
proxy: {
'/xxx/ajax': {
target: 'http://localhost/test',
router: () => {
return `${TARGET_URL}`;
},
pathRewrite: {
'^/xxx/ajax': '/ajax',
},
ws: true,
changeOrigin: true,
},
// ...
},
},
css: {
loaderOptions: {
postcss: {
plugins: [require('autoprefixer')()],
},
less: {
lessOptions: {
modifyVars: {
"lib-b-color": "#xxx",
},
javascriptEnabled: true
}
}
},
},
// ...
configureWebpack: {
resolve: {
alias: {
'@': resolve(NODE_PATH),
'@comm': resolve(`${NODE_PATH}/comm/`),
'@components': resolve(`${NODE_PATH}/components/`),
'@utils': resolve(`${NODE_PATH}/utils/`),
'@router': resolve(`${NODE_PATH}/router/`),
'@views': resolve(`${NODE_PATH}/views/`),
'@mixins': resolve(`${NODE_PATH}/mixins/`),
'@apis': resolve(`${NODE_PATH}/apis/`),
'@plugins': resolve(`${NODE_PATH}/plugins/`),
},
extensions: ['.js', '.vue', '.json', '.ts']
},
module: {
rules: [
{
test: /\.ts?$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ["@babel/preset-typescript"]
}
},
}
]
},
plugins: [
// ... 定义一些全局变量
new webpack.DefinePlugin({
AAA: JSON.stringify(process.env.AAA),
// ...
}),
],
externals: {
vue: 'Vue',
},
},
pages: {
index: {
entry: `./xxx/index/main.js`,
template: `public/index.html`,
filename: `index.html`,
title: 'index',
isProd,
},
preview: {
entry: `./xxx/preview/main.js`,
template: `public/preview.html`,
filename: `preview.html`,
title: 'preview',
isProd,
},
login: {
entry: `./xxx/login/main.js`,
template: 'public/login.html',
filename: 'login.html',
title: 'login',
},
},
transpileDependencies: ["cccc"],
};
module.exports = vueConfig;
babel.config.js:
const presets = [];
// ...
presets.push('@vue/cli-plugin-babel/preset');
// ...
module.exports = {
presets,
};
ok,基本就介绍到这里,下面开始我们的改造流程。
引入#
注意:此时的nodejs版本切换至v18.8.0
vite-plugin-vue2#
vitejs/vite-plugin-vue2: Vite plugin for Vue 2.7 (github.com)
因为我们的vue是2.6.14,所以我们不能用@vitejs/plugin-vue,又因为我们只能使用vite-plugin-vue2
vite#
因为我们只能使用vite-plugin-vue2,而这个依赖仅支持vite5以下的环境,所以这里选择vite4.5.3。
注意此时node版本至少要v18
vite-plugin-html#
这个包对标html-webpack-plugin,主要用来实现多页面 + 动态模板
vite-plugin-require#
因为项目中大量使用了webpack的require,要手动改挺累的,所以找了这个包用来支持这种写法。
dart-sass + sass#
由于生产环境机子nodejs版本过低,所以连带着本机也只能在这个版本上下徘徊,对于sass处理器也只能使用node-sass,这货有多坑爹,相信大家都清楚。
因为要用vite4的要求之一就是nodejs版本要高于18,所以这货是肯定不能用了,自然需要平替。
这里采用dart-sass和sass用来平替。
整理#
devDependencies:
- "node-sass": "@114514"
+ "sass@1.77.2",
+ "dart-sass@1.25.0",
+ "vite@4.5.3",
+ "vite-plugin-html@3.2.2",
+ "vite-plugin-require@1.2.14",
+ "vite-plugin-vue2@2.0.3",配置#
首先我们先去到package.json中,新增指令:
"devite": "cross-env IS_VITE=true vite --mode dev"IS_VITE环境变量用来区分当前是什么环境。注意你一定要同步这个配置项到vue.config.js中的全局变量注册IS_VITE: false,因为生产环境还是使用的webpack编译,如果少了这个变量就会直接报错卡住。
找好依赖并安装之后,我们开始配置vite.config.js
import { defineConfig } from 'vite'
import { createVuePlugin } from 'vite-plugin-vue2'
import { createHtmlPlugin } from 'vite-plugin-html';
// import legacy from '@vitejs/plugin-legacy' // 仅开发模式就不需要这个了
// import babelPlugin from 'vite-plugin-babel'
import requirePlugin from 'vite-plugin-require'
import { resolve } from 'path'
import { config } from 'dotenv'
config();
const env = process.env;
const ISPRO = false;
// ...
const PORT = 1145 //114, 514;
const IS_VITE = env.IS_VITE;
const injectData = {
FILE_VESION: 'src'
}
function genHtmlPlugin(data) {
const tags = [
{
tag: 'script',
injectTo: 'head',
attrs: {
src: '/libs/js/comm/xxx.min.js',
}
},
{
tag: 'script',
injectTo: 'head',
attrs: {
type: 'text/javascript'
},
children: `
if (typeof window !== 'undefined') {
window.global = window;
}
`
},
];
const pages = [
{
entry: 'xxx/index/main.js',
template: 'index.html',
filename: 'index.html',
injectOptions: {
data: {
...data,
title: 'index'
},
tags: [
...tags,
]
}
},
{
entry: 'xxx/preview/main.js',
template: 'preview.html',
filename: 'preview.html',
injectOptions: {
data: {
...data,
title: 'preview'
},
tags: [
...tags,
]
}
},
{
entry: 'xxx/login/main.js',
template: 'login.html',
filename: 'login.html',
injectOptions: {
data: {
...data,
title: 'login'
},
tags: [
...tags,
]
}
},
]
const plugin = createHtmlPlugin({
minify: true,
pages
})
return plugin
}
export default () => defineConfig({
plugins: [createVuePlugin(), genHtmlPlugin(injectData), requirePlugin(),
// legacy({
// targets: ['defaults', 'not IE 11'],
// }),
],
define: {
IS_VITE,
AAA: JSON.stringify('AAA'),
},
mode: 'development',
css: {
postcss: {
plugins: [require('autoprefixer')()],
},
preprocessorOptions: {
less: {
modifyVars: {
"lib-b-color": "#xxx",
},
javascriptEnabled: true
}
},
devSourcemap: true,
},
optimizeDeps: {
exclude: ['vue'],
include: ["cccc"],
},
resolve: {
alias: {
'~': resolve(NODE_PATH),
'@': resolve(NODE_PATH),
'~@': resolve(NODE_PATH),
'@comm': resolve(`${NODE_PATH}/comm/`),
'@components': resolve(`${NODE_PATH}/components/`),
'@utils': resolve(`${NODE_PATH}/utils/`),
'@router': resolve(`${NODE_PATH}/router/`),
'@views': resolve(`${NODE_PATH}/views/`),
'@mixins': resolve(`${NODE_PATH}/mixins/`),
'@apis': resolve(`${NODE_PATH}/apis/`),
'@plugins': resolve(`${NODE_PATH}/plugins/`),
},
extensions: ['.vue', '.mjs', '.js', '.mts', '.ts', '.jsx', '.tsx', '.json'],
},
server: {
port: PORT,
cors: true,
proxy: {
'/xxx/ajax': {
rewrite: path => {
return path.replace(/^\/xxx\/ajax/, '/ajax')
},
target: `${TARGET_URL}`,
ws: true,
changeOrigin: true,
},
// ...
},
}
})说一下里面的一些点:
- 之前我们通过
html-webpack-plugin注入的模板数据,现在我们使用vite-plugin-html里的injectOptions.data注入,每个key对应html文件中的<%- key %>。 define:这里用来定义业务代码中要用到的环境变量,比如我们等会要用IS_VITE区分部分业务代码(登录跳转逻辑)less配置注意去掉了lessOptions的外层optimizeDeps:exclude对应前面的externals,include对应前面的transpileDependencies。'~': resolve(NODE_PATH),'~@': resolve(NODE_PATH),:alias中多了这俩玩意儿,因为代码中存在一些样式文件是这样引入的:@import url('~@/styles/xxx.src.css'),所以需要额外再补充。proxy的规则,vite中proxy是基于http-proxy封装的,它里面的选项可见:https://github.com/http-party/node-http-proxy#options。和之前`vue.config.js`中的写法差不多,但是`router`变成`target`,`pathRewrite`变成`rewrite function`。babel:你可以看到我这没有再配置babel,因为vite自带了babel,包括对ts的处理,所以我们并不需要额外引入。
然后是babel.config.js,由于我们是vite环境,所以不能使用@vue/cli-plugin-babel/preset,我们直接基于环境变量IS_VITE区别即可。
const presets = [];
if (!process.env.IS_VITE) {
presets.push('@vue/cli-plugin-babel/preset');
}
module.exports = {
presets,
};
另外别忘了在你的.eslintrc中将IS_VITE定义为全局变量
这么配置就完成了,然后来说一些兼容性的问题。
兼容性问题#
require#
虽然我们用了vite-plugin-require作为替代,但该插件并不是完全支持webpack require的写法,比如:
const a = require(`./${xxx}.png`) // webpack的可以, vite-plugin-require的不行需要调整为:
const a = require(xxx + '.png');另外如果传入一个变量是绝对不行的(包括webpack require),比如:
const url = "./xxx.jpg";
const img = require(url); // 会报错,因为是变量无法知道执行上下文因为require.context实际上是创建一个执行上下文,执行时再去找,如果传入一个变量,压根找不到。
The arguments passed to
require.contextmust be literals!
文件找错问题#
之前有些不规范写法,将js命名和vue文件命名写成一样的,比如:
\- A.vue
\- A.js然后在B中引入时不带后缀:
import A from './A'这种是webpack可行但vite不行的操作。因为webpack底层做了额外操作用来试错,而vite讲究的是原生。
module.exports#
业务代码中存在module.exports的导出方式,改成export default即可,webpack自身也支持这种写法。
public/index.html重命名#
为啥要这么做,因为vite把index.html视作是源码,所以默认情况下会去找根文件夹里的index.html,整个服务器也是基于根目录。
当然,这不是不可以调整的。然而我们用了vite-plugin-html,这玩意儿bug还挺多,作者也没空改。。。。所以在经过一些列无意义调整后,还是采用重写public里的index.html和preview.html为_index.html以及_preview.html。
同时在根目录里新增index.html、preview.html以及login.html
import Vue from vue#
前面说过项目中有俩vue,一个是node_modules里的,另一个是public/lib里通过script引入的。因为我们配置了externals,所以生产环境是使用lib下的,不过我发现在开发环境下,原来的配置也是用的lib下的,也就是node_modules里的vue压根没用上。
但是到了vite里,我发现它被导入了,也就是说vite先找的node_modules里的而不是script的。
因此原本代码中存在import Vue from 'vue'的写法就都需要去掉,为什么呢?因为lib_c也是要用到这个script版本的vue,然后往Vue.prototype里注入了一些内容。而业务代码中又有用到这玩意儿,所以如果不去掉就会导致找不到global vue prototype里被注入的内容。
跳转登录逻辑#
原来多页面配置是基于vue-cli提供的配置项来配置的,貌似会被整合到路由里。但是通过vite-plugin-html的不行,所以我们需要调整原来的逻辑,这里采用前面全局注入IS_VITE区分
if (IS_VITE) {
window.location.href = window.location.origin + 'login.html'
} else {
window.location.href = window.location.origin + '/login/'
}不过跳回index.html倒是不需要改,因为访问路径没有index.html也会跳到那里去。
未解决的问题#
当前有一个无法解决的问题:生产环境node版本太低,连带着sass预处理器的依赖也只能是node-sass。
也是因为这个问题,我们不能光明正大的添加vite依赖。
以这也导致当前接入实际开发变得有些儿戏,一旦注入了vite,也就意味着得删掉node-sass,一旦删掉node-sass,你的package.json就不能提交到本地了,会直接让生产环境编译失败。
那要怎么办呢?
复杂的操作:#
-
在注入vite之前,备份当前
package.json和lock文件 -
切换node到
v18,注入vite依赖,移除node-sass,添加dart-sass,也做备份 -
如果涉及到包更新,切换
package.json和lock文件,切换node版本,然后更新提交到本地 -
重复步骤1
难顶
分支#
这里推荐开本机分支,平时业务代码可以在vite分支里编写,提交时合并即可(反正一般改动较大的需求也都是开分支,相信大家轻车熟路)。
一旦涉及到依赖更新,切换到master分支即可。
分支注意不要有对应的远程分支,本机即可。
但是这个方案需要考验你的git熟悉度,你不能再像平时那样add .了。
分支衍生
依旧是开分支,前面操作不变,然后把分支的package.json和lock文件都给添加到.gitignore里,这样你又可以愉快的add .了。
目前着实没有好的方案。。
总结#
本次迁移不难,难的是nodejs环境问题。
