背景
基于vue2
已经开发了一个选择器web app,是单页应用。后续需求是要把这个spa变成一个npm组件,因为要考虑其他项目的使用。以下是一些改造的心得以及遇到的坑以及解决方案。
改造前的技术栈
vue2.7.x
+ @vue/cli-service
+ vue-i18n
+ vue-router
+ vuex
+ elementUI2.x
+ axios
除此之外,还安装了一些公司内部开发的包,影响的范围是在打包的时候会对文件做一些操作
评估与改造
平时如果只是封装一个sdk,就只是一些js代码,最多也就把必要的第三方npm包也打包进去,npm包体积不会太大。
但如果是业务组件,要考虑包的大小,考虑到宿主项目的UI框架不兼容,所以也要把UI框架也要打进去,至于其他第三方包,能省则省,该externals掉就externals掉。
盘点了下,以下是可以通过改代码,把以下第三方包直接省掉,以达到减少包大小的效果:
vue-router
,我们可以用component
来替换不同的组件,这样就可以平替掉vue-router
vuex
,可以在根组件使用provide
,为了在子组件、孙子组件inject
后使用保留响应式,需要provide
时,直接传this
。如果有看过ElementUI2.x
源码的同学肯定不陌生,这样就可以平替掉vuex
js
// 父组件
data() {
return {
a: 1,
b: 2
}
},
provide() {
return {
docSelector: this
}
},
js
// 子组件
inject: ['docSelector'],
methods: {
test() {
console.log(this.docSelector.a)
this.docSelector.b = 3
}
}
vue-i18n
,自己实现一个class,一个属性是用记录当前使用的多语言,一个方法是根据keyword读取多语言文件的字段返回值。以及需要写一个方法类似$t
作为mixins
导入到每个SFC
,这样子就可以平替掉vue-i18n
遇到的问题
npm包使用的vue版本跟宿主项目的vue版本不一致导致的坑
这里直接先说结论:npm包需要externals的包,版本要尽可能的低,以兼容宿主项目的包。几个例子,npm项目vue@2.7.x
,而宿主项目的vue
是2.6.x
,这时候,宿主项目打包出来的产物,会有2份vue
代码。因为版本不兼容。
经过评估,因为我们的npm项目依赖了2.6.x的Observer
属性,所以npm项目需要从vue@2.7.x
降级到vue@2.6.0
,并且配置package.json
的peerDependencies
,这个字段表示某个包期望宿主项目中已经存在的依赖,而不是由包本身直接安装。而且npm包打包的时候也要配置externals
掉。
json
{
"peerDependencies": {
"vue": ">2.6.0"
}
}
以这个配置为例:
- 如果宿主项目的
vue
版本>2.6.0
,打包之后,就只会有一份vue
代码; - 如果宿主项目的
vue
版本<2.6.0
,打包之后,就会有两两份vue
代码; - 如果使用
npm link
本地调试,当且仅当npm包的vue版本===
宿主项目的vue版本,才会打包一份vue
代码,否则会有两份vue
代码。举例子说,npm项目vue@2.6.14
以及宿主项目vue@2.6.14
,打包只会有一份vue
代码;npm项目vue@2.6.11
,宿主项目vue@2.6.14
,打包就会有两份vue
代码。
为什么要考虑这个问题?因为遇到了以下的问题,也经历了好几天的debug,通过debugger仔细观察才知道有两份vue
,才去思考为什么。
问题表现:
- 在项目内写的测试demo,可以正常展示
el-tooltip
、el-loading
、el-messages
组件,但是在宿主项目项目npm link
来加载本地包时,其他UI组件都可以正常展示,但是这几个组件不能正常显示。 - 发布正式npm包到私有npm仓库,宿主项目有些安装之后是正常显示,有些是不能正常显示对应的组件
看了el-tooltip
、el-loading
、el-messages
还有类似的源码,发现了会在SFC引入import Vue from 'vue'
,去做一些实现,譬如el-tooltip
就是通过这个方法去做一些trick影响子组件的渲染顺序。所以这里的vue
就会以npm项目的vue版本为准,所以如果版本对不上,就会出现两份。
样式隔离
- 如果UI库自带修改UI库css名前缀,就直接改,不行的话可以写插件,用ast改。见这里:github.com/JimFirst/ch...
- 如果有全局reset样式,也要手动改css名前缀
- vue sfc记得加上scope ->
<script scoped>
知识碎片补充
- 项目是用公司内部的
cli
脚手架搭建的,它实现了一键安装vue-router
vuex
vue-i18n
,但是是使用了require
来导入几个文件实现的。我发现哪怕我没有引用这三个包,但是最终打包还是会打进去,没有tree-shake
掉,所以我自己改掉了用require
引用的方法,才可以把这些包去掉。 - 之前都是直接配置
webpack
配置,对@vue/service
配置不是特别熟悉,所以遇到问题都是直接看源码+debugger,基本都可以解决问题。记录一些代码片段:
json
// package.json
"build:prod": "cross-env NODE_ENV=prod vue-cli-service build --mode production --report --skip-plugins xxxx,yyyy --target lib --name zzzzz --dest libdist libsrc/index.js",
- 上文提到项目依赖了内部的一些npm包,而且会影响打包产物。如果需要跳过这些插件,我发现
vue-cli-service
官方的文档也没有介绍--skip-plugins
这个字段是可以跳过某些插件的,我也是看源码才知道
js
const env = process.env.NODE_ENV;
const path = require("path");
module.exports = {
configureWebpack: (config) => {
// 项目入口配置
config.entry = {
app: path.resolve(__dirname, "./main.js"),
};
config.mode = "production";
},
chainWebpack: (config) => {
// 删除名为 svgs 的规则
config.module.rules.delete("svg");
// 添加新的 svgs 规则
config.module
.rule("svg")
.test(/\.(svg)(\?.*)?$/)
.type("asset/inline");
// 删除名为 images 的规则
config.module.rules.delete("images");
// 添加新的 images 规则
config.module
.rule("images")
.test(/\.(png|jpe?g|gif|webp|avif)(\?.*)?$/)
.type("asset/inline");
// 删除名为 fonts 的规则
config.module.rules.delete("fonts");
// 添加新的 fonts 规则
config.module
.rule("fonts")
.test(/\.(woff2?|eot|ttf|otf)(\?.*)?$/i)
.type("asset/inline");
},
};
- 其实这些图片/字体资源应该要上
CDN
,因为是内部项目,没有CDN
资源申请,所以需要把所有图片/字体都打包进去包里,而且不是抽出来。可以去webpack
官网对asset/inline
的描述:webpack.js.cn/guides/asse...
后续
后续的需求是希望不同的项目都可以接入这个组件,所以后续需要考虑改造成web component
,已知有以下问题需要解决:props
传参失去了响应式,不支持append
到body
所以要做对应的处理。