1.cjsԴ??
2.monorepo多包管理架构实践
3.esModuleInterop 到底做了什么?
4.cookieAPI真难用,你造过相关的轮子吗
5.一次vue-cli4项目升级到vite的经历
6.element-plus源码与二次开发:package.json解析
cjsԴ??
本文将分享如何以Vue3插件的形式快速开发一个轻量级的聊天室,实现直播间聊天功能。首先,明确插件功能范围是关键,接着我们逐步进行开发。大秀源码教程 1. 创建项目并配置:初始化一个空白项目,安装eslint和prettier等代码校验工具,并设置`.eslintrc.cjs`和`.prettierrc.cjs`,注意因项目采用ESM模块,所有js文件需以.cjs结尾。 2. 打包脚本设置:创建scripts文件夹,引入vite进行打包,添加jsx支持,安装@vitejs/plugin-vue-jsx,并在`package.json`中添加打包命令。完整的build脚本请参阅源码链接。 3. Vue3插件入口:编写插件入口函数,它接收用户参数,如登录信息和聊天室标识,以及初始状态。 4. 输入框和消息列表组件:实现输入框功能,包括文本和发送,以及消息列表的渲染和滚动。 5. 聊天室核心功能:管理聊天室状态,初始化环信SDK,挂载监听事件,确保实时通信。 要使用这个插件,只需提供必要的配置参数,即可在页面中快速集成一个聊天室。 查看成果,你可以访问以下地址获取源代码: Github 源码地址 以及npm包地址: npm 相关包地址 参考资料包括环信注册、官方Web端文档,以及前端组件库开发和Vue3组件的TSX编写教程。monorepo多包管理架构实践
平时开发项目时,我们会积累和沉淀许多通用的组件、方法等,它们通常适用于多个项目。但由于不同的项目具有不同的代码库,这些通用组件或方法不能简单地复制粘贴,这也不利于统一维护和更新。
如果我们要在一个代码库中维护一套通用的组件、方法、模板等可能服务于其他项目的 package,这些 package 可以独立发布且互不干扰,我们应该如何操作呢?
最初,我并不知道该如何进行调研,于是翻看了一些优秀库(如 vant、element-plus)的底层架构,大致了解了一些情况。结合自己的理解和认知,我使用 pnpm + vite 搭建了一个可供参考使用的 monorepo 多包管理框架。
为何选择 pnpm:
pnpm 相较于 npm、yarn 可以有效节省磁盘空间并提升安装速度。
pnpm 内置了对单个代码仓库包含多个软件包的支持,是 monorepo 架构模式的不二之选。
原理概括:pnpm 将所有的依赖包的层次提升到同一层次并安装存放在一个统一的地方(.pnpm),创建非扁平的 node_modules 目录,包之间的依赖关系可以很清楚地看到,一个包的依赖包以符号链接的形式存在,符号链接会指向当前包的原始位置,即所在的 .pnpm 的顶层位置。
为何选择 vite:
vite 随着 vue 3.x 而来,极大改善了开发体验,冷启动让人惊艳,再也不用担心项目越来越大启动速度越来越慢了。
平时开发项目基本是哪里弄炒股源码 vue 3.x + vite + typescript 的组合,vite 有一个库模式的构建方式,这意味着我们在开发项目和通用包时,都可以使用 vite,而无需引入第三方构建工具,使用 vite 无疑具有天然优势。
这里存在一个问题,现在普遍使用 typescript 来开发项目,开发的通用库在发布构建时也需要生成相应的 .d.ts 类型声明文件,vite 的库模式构建是不会自动生成 .d.ts 文件的,为了能自动生成类型声明文件我需要用到 tsc 和 vue-tsc,这里先简单提下,后面会用到。
基本骨架:
包的开发阶段可能需要在一个包中引用另一个包以方便调试,单包开发的话可能需要用 npm link 辅助调试,比较麻烦,多包模式下可以简单地使用 workspace。
组件库:
1. 我们需要在项目的根目录下执行 scripts 命令,以执行对应包中的 scripts 命令,需要用到 pnpm --dir 定位到需要执行命令的目录。
注意:为了更加细粒度地控制构建的过程,这里封装了构建的脚手架命令 vswift-cli build,简单直接点的话可以直接用 "build": "rm -r ./dist && vite build && vue-tsc"。
2. 组件库中有独立的 package.json、tsconfig.json、vite.config.ts 配置,package.json 中有几个关键配置:files、main、module、types、exports、publishConfig。
files - 发布的包所包含的内容,默认会带上 package.json
main - require 的入口文件,如 dist/index.umd.cjs
module - import 的入口文件,如 dist/index.js
types - 类型声明的入口文件,如 dist/types/index.d.ts
exports - 定义导出模块的路径别名
publishConfig - 发布配置
3. tsconfig.json 主要是配置 typescript 的作用范围 include,以及类型声明文件的编译输出。因为是组件库,所以会有 .vue 文件,vue-tsc 可以生成对应的 .vue.d.ts 类型声明文件。
4. vite.config.ts 是库模式 build 的配置,基本配置如下:
5. 最终输出
原始的文件目录
打包输出的文件目录
组件库打包可能存在的问题
解决方法:引入 vite-plugin-css-injected-by-js 插件解决。
以 element-plus 开发场景为例,如果开发的组件库是在 element-plus 组件的基础上开发的,打包后用到的样式会被提取到一个通用的 .css 文件中,这其中包括用到的 element-plus 根(:root)样式变量,这可能会覆盖掉项目中的 :root 样式变量。
解决方法:一个不错的解决方法是根据组件库的名称自定义 element-plus 命名空间,如何自定义命名空间,element-plus 官方文档有详细说明,这里不做赘述。这里有一点需要注意下,我们是打包自己的组件库,命名空间的设置不能是在 App.vue 根组件中了,而在你需要打包的组件中。
方法库:
不同于组件库是以 .vue 文件为主的视觉交互,通用方法中主要是以 .ts 文件为主的通用逻辑方法等,所以它和组件库最大的区别在于生成类型声明文件的工具,组件库是用 vue-tsc,通用方法库则是用 tsc,可参见上面的 vswift-cli build 脚本构建命令,简单直接点的话可以直接用 "build": "rm -r ./dist && vite build && tsc"。
脚手架:
封装自己的脚手架,有 3 个关键要点:bin 配置、files 配置、cli 顶部声明。
脚手架 package 基本结构预览 ------------>
这里解释下 "build": "pnpm clean && tsc && scp -r ../vswift-templates/src/templates ./dist" 的京东小助手源码含义-----------> “pnpm clean && tsc” 无需多说,“scp -r ../vswift-templates/src/templates ./dist” 是将 vswift-templates/src/templates 中维护的通用模板拷贝到打包输出目录 dist 中,并随着 @vswift/cli 包一起发布,使用脚手架命令快速创建基础页面的模板将从 “dist/templates” 中获取。
name - 发布后的包名,全局安装:npm install @vswift/cli -g
bin.vswift-cli - 全局安装 @vswift/cli 后,即可在命令行使用 vswift-cli
files - 发布的文件别漏掉 bin.js
bin.js - 顶部需加 #!/usr/bin/env node 声明
注意:脚手架不需要 vite.config.ts 配置,只需在 tsconfig.json 中稍加区别修改,执行 tsc 使用。
outDir - 编译后的输出目录
declaration - 执行 tsc 时输出 .d.ts 声明文件
declarationDir - .d.ts 声明文件的输出目录
最终输出目录 ----------------->
这里推荐几个开发脚手架时常用的 package -------------------->
commander - Node.js 命令行插件,可以格式化输出友好的命令符提示
nanospinner - Node.js 终端微调器,进度、成功、失败等状态友好展示,也可用 ora 代替
consola - 用于 Node.js 和浏览器的优雅控制台记录器
chalk - 终端样式粉笔,着色高亮
vswift-cli build 构建命令 ------------------>
vswift-cli create 根据模板创建基础页面命令 --------------------->
vswift-cli dev 启动组件库预览调试命令 -------------------->
cli 命令行逻辑 --------------->
自动生成的 vswift-cli 命令行 help ----------------------->
发布包:
每个 package 的 package.json 中都有 release 命令,用于发布当前 package,package 会在发布前先 build 操作生成 dist 打包目录,最终发布的内容为 package.json 中 files 配置目录文件,当前 package 的 package.json 会被自动添加进去。
这里使用了 release-it 插件,以方便快速地进行发布。
还需注意,package.json 中要有相应的发布配置 --------------->
架构源码参考:
esModuleInterop 到底做了什么?
很多 React 开发者在从 JavaScript迁移到 TypeScript(TS)时,会遇到一个关于导入问题的困惑。在 JavaScript中,引入React模块通常是这样的:
然而,在 TypeScript中,引入方式却变成了这样:
当尝试在TypeScript中模仿JavaScript的导入方式时,编辑器会报错,指出该模块是由 "export =" 声明的,仅在启用 "esModuleInterop" 标志时与默认导入一起使用。要解决这个问题,需要在 `tsconfig.json` 文件中设置 `compilerOptions.esModuleInterop` 为 `true`。
理解这一问题的关键在于了解JavaScript的模块系统。常用的JavaScript模块系统有三种,其中AMD模块系统已经较为少见,故略过。在TypeScript和Babel编译器中,更倾向于使用CommonJS(CJS)模块。默认情况下,代码中表示的ES模块(ESM)都会被转换为CJS模块。
回到开头的问题,打开React库的`index.js`文件,可以发现React基于CJS模块,等效于:
而`index.ts`文件中,写入一段代码:
编译后的代码为:
因此,打印结果为`undefined`,因为`react`模块的`module.exports`中没有`default`属性,后续获取`React.createElement`和`React.Component`等函数时自然会报错。
这一问题引申出的是,大量现有的第三方库大多使用UMD或CJS模块,而前端代码几乎都是使用ESM模块。因此,ESM和CJS模块之间需要一套规则来实现兼容。
在TypeScript中,默认的导入转换规则为:
而对于`export`变量的转换规则为:
在启用`esModuleInterop`属性后,TypeScript对于导入的转换规则发生了变化(`export`规则保持不变):
这里,对于默认导入和命名空间(`*`)导入,TypeScript使用了两个辅助函数来协助转换。
首先,`__importDefault`函数做的事情是:
比如上面的导入语句,编译后再层层翻译:
这样就成功获取了`react`模块的`module.exports`。
接下来是`__importStar`函数,它做的事情是:
(对`__importStar`的层层翻译分析过程省略)
在默认情况下,Babel的转换规则与启用`esModuleInterop`的TypeScript情况相似,同样通过两个辅助函数来处理。
关于`_interopRequireDefault`和`_interopRequireWildcard`函数,立体指标公式源码它们分别类似`__importDefault`和`__importStar`。
在特殊的Webpack环境中,通常情况下,Babel和TypeScript会一起使用Webpack。而Webpack与TypeScript的结合有两种方式:
如果使用`ts-loader`,Webpack会先将源代码交给TypeScript编译器(tsc)进行编译,然后处理编译后的代码。编译后的所有模块都会变为CJS模块,因此Babel不会进行处理,直接交给Webpack以CJS方式处理模块。
如果使用`@babel/preset-typescript`,Webpack不会调用tsc,忽略`tsconfig.json`配置,而是直接使用Babel编译TS文件。这个编译过程相比调用tsc轻得多,因为Babel只会简单移除所有TS相关代码,不做类型检查。在这种情况下,一个TS模块通过Babel的`@babel/preset-env`和`@babel/preset-typescript`两个预设处理。后者的工作很简单,仅去除所有TS相关代码,不处理模块,前者则将ESM转换为CJS。然而,Webpack的`babel-loader`在调用`babel.transform`时,传入了`caller`选项:
这导致Babel保留了ESM的`import`和`export`语法。
Webpack为模块提供了一个runtime机制,使得Webpack在模块闭包中注入代表`module require`和`exports`的变量,因此Webpack处理模块对于自身而言较为自由。
在CJS引用ESM的场景中,Webpack的编译机制较为特别,通过`_webpack_require__`类似于`require`,返回目标模块的`module.exports`对象。`_webpack_require__.n`函数接收一个参数对象,返回一个对象,该返回对象的`a`属性(其确切名称未知)会被设定为参数对象。因此,上述源代码的`console.log(cjs)`会打印出`cjs.js`的`module.exports`。
总结:当前许多常用的包基于CJS/UMD开发,而前端代码主要使用ESM,常见场景是ESM导入CJS库。由于ESM和CJS在概念上存在差异,最大的差异在于ESM有`default`概念而CJS没有,因此在`default`上会遇到问题。TypeScript、Babel、Webpack都有各自的处理机制来解决这个兼容问题,核心思想基本都是通过添加和读取`default`属性来实现。
cookieAPI真难用,你造过相关的轮子吗
前言
歌德说过:读一本好书,就是在和高尚的人谈话。同理,读优秀的开源项目的源码,就是在和优秀的大佬交流,是站在巨人的肩膀上学习——今天我们将通过读js-cookie的源码,来学会造一个操作cookie的轮子~
1.准备简单介绍一下cookieCookie是直接存储在浏览器中的一小串数据。它们是HTTP协议的一部分,由RFC规范定义。最常见的用处之一就是身份验证我们可以使用document.cookie属性从浏览器访问cookie。
这个库,是干啥的?不用这个库时?cookie的原生API,非常“丑陋”:
修改我们可以写入document.cookie。但这不是一个数据属性,它是一个访问器(getter/setter)。对其的赋值操作会被特殊处理。对document.cookie的写入操作只会更新其中提到的cookie,而不会涉及其他cookie。java 源码比较工具例如,此调用设置了一个名称为user且值为John的cookie:
document.cookie?=?"user=John";?//?只会更新名称为?user?的?cookiedocument.cookie?=?"user=John;?path=/;?expires=Tue,??Jan??::?GMT"赋值时传入字符串,并且键值对以=相连,如果多项还要用分号;隔开...
删除将过期时间设置为过去,自然就是删除了~
//?删除?cookie(让它立即过期)document.cookie?=?"expires=Thu,??Jan??::?GMT";document.cookie?=?"user=John;?max-age=0";但是很明显,这语义化也太差了..
js-cookieAPI我们先来了解一下API
//?setCookies.set('name',?'value',?{ ?expires:?7,?path:?''?})//?get?Cookies.get('name')?//?=>?'value'Cookies.get()?//?=>?{ ?name:?'value'?}//?removeCookies.remove('name')OK我们大概可以知道是这样子
set(key,?value)get(key)remove(key)简洁方便多了,并且一眼就知道这行代码是在干什么~
2.读源码三部曲?这段可能有点太细了,如果嫌啰嗦,只想看实现可以直接跳到下面的实现部分~
一?READMEwhy一个简单、轻量级的JavaScriptAPI,用于处理cookie适用于所有浏览器?接受任何字符大量的测试?不依赖支持ES模块支持AMD/CommonJSRFC兼容的有用的Wiki?启用自定义编码/解码<字节gzip!
优点多多呀
表示后文会详细提及~BasicUsage大概就是前面写过的API介绍
二package.json依赖确实是很少依赖,并且只有开发依赖,没有生产依赖,很nice~
scripts"scripts":?{ "test":?"grunt?test","format":?"grunt?exec:format","dist":?"rm?-rf?dist/*?&&?rollup?-c","release":?"release-it"?},exportsexports":?{ ".":?{ ?"import":?"./dist/js.cookie.mjs",?"require":?"./dist/js.cookie.js"},看来入口在/dist/js.cookie这点从index.js也能看出
module.exports?=?require('./dist/js.cookie')当然,目前是没有dist这个目录的。这需要打包~
.mjs另外我们刚才看到了.mjs这个后缀,这我还是第一次见,你呢
.mjs:表示当前文件用ESM的方式进行加载
.js:采用CJS的方式加载。
ESM和CJSESM是将javascript程序拆分成多个单独模块,并能按需导入的标准。和webpack,babel不同的是,esm是javascript的标准功能,在浏览器端和nodejs中都已得到实现。也就是熟悉的import、exportCJS也就是commonJS,也就是module.exports、require。
更多介绍以及差别不再赘述~
三src进入src,首当其冲的就是api.mjs,这一眼就是关键文件啊?emm..一个init方法,其中包含set和get方法,返回一个Objectremove方法藏在其中~乍一看,代码当然还是能看得懂每行都是在做啥的呀~但是总所周知?开源项目也是不断迭代出来的~也不是一蹴而就的——若川哥
okok,我们来一步步"抄"一下源码
3.实现?下面为了传参返回值更加清晰用了TS语法~
3.1最简易版本set设置一个键值对,要这样
document.cookie?=?`${ key}=${ value};?expires=${ expires};?path=${ path}`除了键值对还有后面的属性~可别把它忘记了我们用写一个接口限制一下传入的属性:
interface?Attributes?{ ?path:?string;?//可访问cookie的路径,默认为根目录?domain?:?string;?//可访问?cookie?的域?expires?:?string?|?number?|?Date?//?过期时间:UTC时间戳string?||?过期天数?[`max-age`]?:number?//ookie?的过期时间距离当前时间的秒数?//...}const?TWENTY_FOUR_HOURS?=?e5?//h的毫秒数//源码中是init的时候传入defaultAttributes,这里先暂做模拟const?defaultAttributes:?Attributes?=?{ path:?'/'}function?set(key:?string,?value:?string,?attributes:?Attributes):?string?|?null?{ ?attributes?=?{ ...defaultAttributes,?...attributes}?//?if?(attributes.expires)?{ //如果有过期时间//?如果是数字形式的,就将过期天数转为?UTC?stringif?(typeof?attributes.expires?===?'number')?{ ?attributes.expires?=?new?Date(Date.now()?+?attributes.expires?*?TWENTY_FOUR_HOURS)?attributes.expires?=?attributes.expires.toUTCString()}?}?//遍历属性键值对并转换为字符串形式?const?attrStr?=?Object.entries(attributes).reduce((prevStr,?attrPair)?=>?{ const?[attrKey,?attrValue]?=?attrPairif?(!attrValue)?return?prevStr//将key拼接进去prevStr?+=?`;?${ attrKey}`//?attrValue?有可能为?truthy,所以要排除?true?值的情况if?(attrValue?===?true)?return?prevStr//?排除?attrValue?存在?";"?号的情况prevStr?+=?`=${ attrValue.split(';?')[0]}`return?prevStr?},?'')?return?document.cookie?=?`${ key}=${ value}${ attrStr}`}get//?删除?cookie(让它立即过期)document.cookie?=?"expires=Thu,??Jan??::?GMT";document.cookie?=?"user=John;?max-age=0";0我们知道document.cookie长这个样子,那么就根据对应规则操作其字符串获得键值对将其转化为Object先
//?删除?cookie(让它立即过期)document.cookie?=?"expires=Thu,??Jan??::?GMT";document.cookie?=?"user=John;?max-age=0";1要注意的有意思的一个点是,可能value中就有'='这个字符,所以还要特殊处理一下~
比如他就是"颜文字==_="?(~~应该不会有人真往cookie里面放表情吧hh~~但是value中有'='还是真的有可能滴~?其实一开始我真没想过这个问题,是看了源码才知道的
Record接收两个参数——keys、values,使得对象中的key、value必须在keys、values里面。
removeremove就简单啦,用set把过期时间设置为过去就好了~
//?删除?cookie(让它立即过期)document.cookie?=?"expires=Thu,??Jan??::?GMT";document.cookie?=?"user=John;?max-age=0";.2接受任何字符从技术上讲,cookie的名称和值可以是任何字符。为了保持有效的格式,它们应该使用内建的encodeURIComponent函数对其进行转义~再使用ecodeURIComponent函数对其进行解码。还记得README中写的接收任何字符吗~这就需要我们自己来在里面进行编码、解码的封装~
set//?删除?cookie(让它立即过期)document.cookie?=?"expires=Thu,??Jan??::?GMT";document.cookie?=?"user=John;?max-age=0";3get//?删除?cookie(让它立即过期)document.cookie?=?"expires=Thu,??Jan??::?GMT";document.cookie?=?"user=John;?max-age=0";.3封装编码和解码两个操作源码中converter.mjs封装了这两个操作为write和read,并作为defaultConverter导出到api.mjs,最后作为converter传入init——降低了代码的耦合性,为后面的自定义配置做了铺垫~前面编码解码变成了这样:
//?删除?cookie(让它立即过期)document.cookie?=?"expires=Thu,??Jan??::?GMT";document.cookie?=?"user=John;?max-age=0";.4启用自定义编码/解码我们是具有内置的encodeURIComponent和decodeURIComponent,但是也并不是必须使用这两个来进行编码和解码,也可以用别的方法——也就是前面README中说的可以自定义编码/解码~除了这两个方法可自定义,其余的属性也可以自定义默认值,并且配置一次后,后续不用每次都传入配置——所以我们需要导出时有对应的两个方法
//?删除?cookie(让它立即过期)document.cookie?=?"expires=Thu,??Jan??::?GMT";document.cookie?=?"user=John;?max-age=0";6封装在其中,利用对象合并时有重复属性名的情况是后面的覆盖掉前面的这一特性完成该自定义配置属性以及转换方法的功能。现在的cookie大概是这样的一个对象
//?删除?cookie(让它立即过期)document.cookie?=?"expires=Thu,??Jan??::?GMT";document.cookie?=?"user=John;?max-age=0";.5防止全局污染现在的cookie直接在全局上下文下,很危险,谁都能更改,而且还不一定能找到,我们将其设置为局部的,封装到init函数中,调用init传入相应的自定义属性以及自定义转换方法得到一个初始化的cookie对象现在大概就是源码的架构形状了~
//?删除?cookie(让它立即过期)document.cookie?=?"expires=Thu,??Jan??::?GMT";document.cookie?=?"user=John;?max-age=0";.6确保一些属性不会给改变用Object.create来生成对象,并用Object.freeze把对象atributes和converter冻结。
//?删除?cookie(让它立即过期)document.cookie?=?"expires=Thu,??Jan??::?GMT";document.cookie?=?"user=John;?max-age=0";9Obecj.create的第二个参数
属性描述符
现在你就不能修改Cookie的attributes、converter属性了~
4.总结&收获?总结init及其中属性&返回而用init函数生成对象是为了解决全局污染问题,并且更新对象时也是用的init现在你再回头看源码是不是就更加清晰了~
扩展说到cookie这个在浏览器中存储数据的小东西,就不得不提一下localstorage、sessionStorage
cookie、localstorage、sessionStorage的区别Web存储对象localStorage和sessionStorage也允许我们在浏览器上保存键/值对。
那他们的区别呢
在页面刷新后(对于sessionStorage)甚至浏览器完全重启(对于localStorage)后,数据仍然保留在浏览器中。默认情况下cookie如果没有设置expires或max-age,在关闭浏览器后就会消失
与cookie不同,Web存储对象不会随每个请求被发送到服务器,存储在本地的数据可以直接获取。因此,我们可以保存更多数据,减少了客户端和服务器端的交互,节省了网络流量。大多数浏览器都允许保存至少2MB的数据(或更多),并且具有用于配置数据的设置。
还有一点和cookie不同,服务器无法通过HTTPheader操纵存储对象。一切都是在JavaScript中完成的。
以及..他们的原生API比cookie的"好看"太多~[doge]
CookiesessionStoragelocalstorage生命周期默认到浏览器关闭,可自定义浏览器关闭除非自行删除或清除缓存,否则一直存在与服务器通信/post/一次vue-cli4项目升级到vite的经历
背景
使用vue-cli4的项目,业务写多了之后开发运行和打包都慢了很多,为了提升开发体验以及更新团队技术框架,需要升级到更高级的脚手架上,两种方案:一是升级到vue-cli5,二是升级到最新的vite。
其中第一种方案升级简单,经过实验,打包的速度不升反慢,这可能和项目中的有依赖以及业务代码有关。
第二种方案升级vite,经过可行性调查,升级到vite的成功率非常高,最后决定从vue-cli4升级到vite,这是一个高风险高回报的事情,因为尽管市面上已存在很多升级成功的案列,但是每个项目都不一样,我们的项目也很庞大,依赖很多,并没有%升级成功的把握。而升级成功的回报也很显而易见,开发环境几乎秒运行,开发体验得到了显著提升。
升级前后对比||vue-cli4|vite||---|---|---||开发启动时间|ms|ms||生产打包时间|s|s||打包体积|.6MB|.6MB||运行期间同一代码改动编译时间|ms|瞬时|
生产环境打包时间可能和我们项目中用到了太多vite插件有关系,但开发环境的提升非常显著。
项目状况项目中用到的Vue2,VueCli版本:4.5.,版本更新时间为.5.8,vueCli4的最后版本为4.5.(.3.),依赖的webpack版本为^4.0.0
组件库使用vant,依赖Less预处理器,通过vue.config.js配置设置了less主题色,在webpack仅支持less-loader@5版本以及对应的less版本
业务css预处理器为stylus:"^0..5",对应stylus-loader:"^3.0.2"
进行了多页打包(MPA)
使用了workbox-webpack-plugin插件配置了PWA:WorkboxWebpackPlugin
配置了多个路径映射(alias别名)
指定了文件输出路径以及hash配置
生产环境下关闭productionSourceMap以及css的sourceMap提升打包速度
proxy开启多个代理
用到了.env文件中的环境变量
按照开发规范忽略部分文件后缀以及index.js
移除了preload脚本
期望结果可以使用vite进行开发和打包
仍保留webpack打包功能(因为项目太大,不能保证升级到vite后会不会有问题,所以仍希望webpack原本功能正常运行)
准备工作升级Node版本,vite只支持node及以上,建议升级到v以上。
安装pnpm工具,pnpm作为更好的npm依赖管理工具,是目前npm和yarn的最好替代品,且有些依赖包使用npm安装时会有异常,使用pnpm安装可解决:pnpm
小项目尝试一键转换升级:wp2vite、webpack-to-vite,这两个工具都提供了一键将webpack项目转成vite的能力,但对于大中型项目,并不可靠。
开始行动1.安装必要依赖pnpm?add?vite-plugin-env-compatible?vite-plugin-html?vite?vite-plugin-vue2?--devvite-plugin-vue2是处理vue2版本代码的插件,如果项目中是vue3,安装的依赖有所不同,请参考webpack-to-vite
2.复制html到根目录,并修改 注意是复制,并只改动复制后的html,这样才不会破坏原有webpack功能。修改复制后的html,增加对应的js文件引用,注意type属性不能少!
<!--?忽略一些代码?--><body><div?id="app"></div>++?<script?type="module"?src="/src/main.js"></script>?++</body>多页打包(MPA),其他页面的html同样操作,不同html引入对应的js即可。
3.新增vite.config.js文件,开始迁移最重要的配置部分空配置如下:
import?{ ?defineConfig?}?from?'vite'export?default?defineConfig({ })4.修改环境变量环境变量主要面临两个问题:
要兼容webpack和vite的环境变量用法
解决方法:使用vite插件vite-plugin-env-compatible,让vite中可以使用webpack中读取环境变量的方式,再配合envPrefix配置,让vite可以读取到VUE_APP_开头的环境变量:
?pnpm?add?vite-plugin-env-compatible?-D?import?{ ?defineConfig?}?from?'vite'?import?envCompatible?from?'vite-plugin-env-compatible'?export?default?defineConfig({ plugins:?[?envCompatible()],envPrefix:?['VUE_APP_']?//?很重要?})?//?mian.js测试?console.log(process.env.VUE_APP_UNION_STATS)?console.log(import.meta.env.VUE_APP_UNION_STATS)两个打印都得到了正确的结果,注意:vite中默认只能读取到VITE_开头的环境变量,如果不配置envPrefix,则会导致第二个打印为undefind。
vite.config.js中不能读取到环境变量
vite.config.js是无法直接通过import.meta.env和process.env获取环境变量的,我们需要通过vite的loadEnv获取。
我们需要将vite.config.js的导出对象改为函数:
?import?{ ?defineConfig,loadEnv?}?from?'vite'?export?default?({ ?mode?})?=>?{ ?const?isPro?=?mode?===?'production'?//?我们可以通过mode直接判断当前是不是生产环境,注意mode可以在运行指令中指定:`vite?build?--mode?master`,如果没有指定,那默认打包就是production?function?getEnv(key)?{ ?//?定义获取环境变量的方法?return?loadEnv(mode,?process.cwd(),'')[key]?//?第三个参数非常重要,下面有详解?}?return?defineConfig({ ?base:?getEnv('VUE_APP_PUBLICPATH'),?//?读取环境变量?//?...忽略其他代码?})?})loadEnv有三个参数,前两个参数基本固定不变,而第三个参数默认情况下是不需要传的,只有在配置了envPrefix项,读取非VITE_开头的变量时才需要,在loadEnv源码中我们可以看到,第三个参数是prefixes:string|string[]='VITE_',也就是环境变量的前缀,默认是VITE_。
如果你的项目和我一样,读取了VUE_APP_PUBLICPATH这样非VITE_开头环境变量,就在loadEnv的第三个参数传递空字符串即可,这样就能读取到所有的环境变量了。
5.兼容commonjs代码项目中有用到commonjs规范的依赖,比如letmd5=require('js-md5').create(),webpack是基于node开发的,支持require语法,在打包的时候webpack也会正确处理这部分代码,但在vite中不会,所以需要将这部分代码改成importmd5from'js-md5'
项目开发环境下,一些node_modules中的包也会存在commonjs的代码,我们可以通过vite的插件?vite-plugin-commonjs来实现这部分代码的转化,保证开发环境的正常运行。
pnpm?add?@originjs/vite-plugin-commonjs?--dev//?vite.config.js?忽略其他代码import?{ ?viteCommonjs?}?from?'@originjs/vite-plugin-commonjs'export?default?({ ?mode?})?=>?{ return?defineConfig({ plugins:?[//?...viteCommonjs()?//?兼容vite中的cjs导入语法]})})6.解决css预处理的问题vite内置了对主流css预处理器的支持(sass/less/stylus),项目使用预处理器时,只需要安装对应预处理依赖即可:
#?.scss?and?.sasspnpm?add?sass?-D#?.lesspnpm?add?stylus?-D#?.styl?and?.styluspnpm?add?stylus?-D比较巧的是,我们项目中用到的Stylus的@import别名的语法和vite冲突,@import'~@/public/stylus/mixins'这样的代码是会报错,一开始我找到了插件,可以帮助我们解决这个问题:vite-plugin-stylus-alias,但是都后面打包的时候发现这个插件有副作用,后面采取了其他方法解决。
<!--?忽略一些代码?--><body><div?id="app"></div>++?<script?type="module"?src="/src/main.js"></script>?++</body>0<!--?忽略一些代码?--><body><div?id="app"></div>++?<script?type="module"?src="/src/main.js"></script>?++</body>1使用这个插件会导致无法生成sourcemap文件,在打包的时候可以看到警告:Sourcemapislikelytobeincorrect:aplugin(vite-plugin-stylus-alias)wasusedtotransformfiles,butdidn'tgenerateasourcemapforthetransformation.Consulttheplugindocumentationforhelp,鉴于插件作者已经很久没有更新,建议能改成相对路径还是直接改,如果引用地方较多,可以定义文件为全局styl文件最新解决方案:一般出现这个报错是因为插件使用了vite的transformapi转换代码,但是return值缺失导致,解决方法:复制插件代码到项目中,在插件transform函数return的结果中,返回map:null,然后再vite.config.js中引用项目中修改后的插件,即可完美解决,如下:文末解决bug有细说
<!--?忽略一些代码?--><body><div?id="app"></div>++?<script?type="module"?src="/src/main.js"></script>?++</body>2定义stylus全局文件
<!--?忽略一些代码?--><body><div?id="app"></div>++?<script?type="module"?src="/src/main.js"></script>?++</body>3这里需要注意,官方文档中css-preprocessoroptions写的是使用文件名拓展名作为key,stylus的文件拓展名是styl,但是我使用了stylus作为key并不会有问题,相反使用styl作为key则不生效了,后续这个地方可以留意一下。
在vite源码中,stylus和styl都进行了判断,理论上都可以使用,但目前测试的结果就是styl作为key不生效,可能源码中其他地方还能找到原因。
7.组件库按需导入和定制主题我们项目中用到的组件库是Vant2,该组件库依赖Less,以及通过配置文件来定制组件的主题,在配置中我们需要进行修改:
vue-cli中的主题配置部分如下:
<!--?忽略一些代码?--><body><div?id="app"></div>++?<script?type="module"?src="/src/main.js"></script>?++</body>4vite中主题配置部分如下:
<!--?忽略一些代码?--><body><div?id="app"></div>++?<script?type="module"?src="/src/main.js"></script>?++</body>5按需导入项目中按需导入vant组件库,组件可以成功导入,但是组件的样式缺失了,这是因为在webpack中,babel-plugin-import插件帮我们实现了组件的样式导入,在vite中使用?vite-plugin-style-import插件帮我们实现这个功能,不仅vant组件库,其他诸如element、antv等组件库也可以使用这个插件进行按需导入:
<!--?忽略一些代码?--><body><div?id="app"></div>++?<script?type="module"?src="/src/main.js"></script>?++</body>6<!--?忽略一些代码?--><body><div?id="app"></div>++?<script?type="module"?src="/src/main.js"></script>?++</body>.修改alias别名配置,以及忽略文件后缀vite配置别名的方法和vue-cli有所不同,且没有默认的别名,都需要通过配置实现,且vite默认不能忽略文件后缀导入,我们也需要通过修改配置来实现:
<!--?忽略一些代码?--><body><div?id="app"></div>++?<script?type="module"?src="/src/main.js"></script>?++</body>8需要注意extensions配置的顺序,从左到右进行匹配,如果存在同名但类型不同的文件,很可能得到期望外的结果,比如同目录下存在index.js和index.vue,按上面的顺序,import'./index?会优先匹配到index.js文件。这种情况建议补全后缀进行导入。
9.配置前端跨域vite配置跨域和webpack也有出入,需要修改配置
<!--?忽略一些代码?--><body><div?id="app"></div>++?<script?type="module"?src="/src/main.js"></script>?++</body>9以上就是几种常见的跨域配置方式,webpack中的devServer改为了server,webpack的proxy中的pathRewrite改成了rewrite,并且类型成为了函数,在函数中返回请求的路径即可。
.多页打包以及打包的其他配置vite在build.rollupOptions配置多页打包,参考rollupOptions,其他配置参考文档
import?{ ?defineConfig?}?from?'vite'export?default?defineConfig({ }).配置运行路径base是指项目运行在服务器的哪个路径下,一般通过从环境变量中动态获取。
import?{ ?defineConfig?}?from?'vite'export?default?defineConfig({ }).配置EsLintvite中使用vite-plugin-eslint插件实现eslint的检查功能,安装过程中发现vite-plugin-eslint@1.4.0会报错,安装1.3.0版本即可。
import?{ ?defineConfig?}?from?'vite'export?default?defineConfig({ })2import?{ ?defineConfig?}?from?'vite'export?default?defineConfig({ }).使用插件@vitejs/plugin-legacy兼容低版本浏览器plugin-legacy文档
import?{ ?defineConfig?}?from?'vite'export?default?defineConfig({ }).配置运行指令import?{ ?defineConfig?}?from?'vite'export?default?defineConfig({ })5可以在指令中声明mode环境,这个mode在vite.config.js中可以得到,具体可以参考前面4.修改环境变量
保存运行指令npmrunserve开发环境已经可以跑起来了,但是vite的特性是你用到的页面才会进行打包,其他页面没有进行访问,是不会打包的,所以需要进行打包才能知道其他地方改造会不会有问题,打包如果有报错,再解决报错即可。
.解决报错globalisnotdefined
这个错误是在node_modules/buffer/index.js?v=ea7文件中抛出的,我查看了yarn.lock文件,依赖路径为多个vue-cli插件>webpack@4.0.0>node-libs-browser?>buffer
这个是依赖的问题,第一删除node_modules重新安装依赖。第二更换npm工具为pnpm重装依赖,如果仍不行,建议不建议webpack,删除掉webpack相关依赖。
网上还有一种做法是:在window对象上挂载global对象,可作为备选方案。
import?{ ?defineConfig?}?from?'vite'export?default?defineConfig({ })6@import'~@vant/icons/src/encode-woff2.less';报错
这个错误是vant组件库中的icon组件抛出的,vite默认不能使用别名,我们在前面配置了别名,但是配置的是~@指向项目中的src目录,这样vant组件库的这个文件引用就找不到了。
issuesvant
解决方法:针对vant的这个文件,做一个别名,放到第一位,优先进行匹配:
import?{ ?defineConfig?}?from?'vite'export?default?defineConfig({ })7运行vitebuild,css产生了一些警告。
警告分为两种,一是css中的属性拼写错误,诸如:color写成了colo,background写成了backgrounc,属于语法错误,根据警告提示搜索对应样式进行修改即可。
二是一些语法正确,但还不清楚为什么压缩时提示了警告:比如stylus修改scoped样式用的>>>语法,以及background?rgba(0,0,0,0.5)提示Unexpected"rgba("。
Thepackagemayhaveincorrectmain/module/exportsspecifiedinitspackage.json
这个错误是通过npminstall后运行vite指令报错的,用yarn安装一直很正常,原因是node_modules的某个包的package.json定义的main入口路径错了,所以找不到模块导入。
解决方法:针对这个模块,定义别名,指向正确的入口:
import?{ ?defineConfig?}?from?'vite'export?default?defineConfig({ })8打包后导入函数定义别名后,调用函数报错
import*asminByfrom'lodash.minby'代码报错了,而且只有在生产环境下才产生。
解决方法:暂时去掉别名,这应该和lodash的导出方式modules.export在vite中转化成ES语法的过程有关系。
importminByfrom'lodash.minby'
requireisnotdefine,通过require导入资源报错
前面在步骤5已经用了插件vite-plugin-commonjs转化common.js的代码了,require理应不会报错。
但是require导入资源算是webpack的功能,和js代码不一样,所以导入资源的代码都要进行修改:
require('./images/logo.png')改成importlogofrom'./images/logo.png'
这样的修改可以兼容webpack和vite,这是在现有项目中,改动的地方会达到上百处,非常麻烦。
element-plus源码与二次开发:package.json解析
element-plus使用pnpm的workspace来搭建monorepo工程,允许在单一码仓库中集中管理大量互相依赖的包,同时确保发布时的独立性。pnpm-workspace.yaml文件在根目录声明内部可引用的包,执行pnpm i后,会在node_modules中创建软连接,无需手动link。
element-plus组件库将vue声明在peerDependencies中,避免在主项目安装组件库时额外安装vue。通过czg包定义规范,执行提交commit命令,确保遵循git规范。使用play子包进行简单的开发调试,引入本地组件库。通过gen命令快速创建新组件,使用模板生成组件基础模板。生成版本号文件命令用于在构建时提供rollup的banner参数,部署前更新版本号命令从环境变量获取TAG_VERSION和GIT_HEAD,写入到三个包的package.json中的version和gitHead字段。
清理dist目录命令使用pnpm run -r --parallel,以并行方式执行所有子包的命令,删除根目录下的dist目录,并执行所有子包的clean命令。构建文档和组件库的关键步骤包括使用rollup执行构建,通过@esbuild-kit/cjs-loader将esm和ts实时转换为CommonJS。生成类型声明文件和代码提示文件,复制源样式文件、编译为css、压缩,并输出到特定目录。启动组件库文档docs项目基于vitepress,构建组件文档,本地测试构建出的生产环境docs,生成多语言文件和CROWDIN_TOKEN。
执行各包的stub命令,使用unbuild打包工具,基于rollup,支持typescript,支持生成commonjs和esmodule和类型声明,无需额外配置。prepare Husky钩子脚本确保自动执行预定义命令,执行pnpm i后,自动执行pnpm stub,编译internal下的三个包入口。
通过上述详细解析,我们可以清晰了解到element-plus源码与二次开发中的核心功能与流程,从构建结构到构建流程,再到二次开发工具的运用,展示了其高效、灵活的特点。
新款vue-cli之create-vue源码阅读总结
新款Vue CLI之create-vue源码阅读总结
create-vue,作为Vue项目的简便启动工具,源码简洁明了。本文将对其核心知识点进行整理。
使用方式:create-vue通过运行outfile.cjs文件,此文件由package.json中的bin配置指定。
在package.json设置type: 'module',表示如果js文件采用ES模块格式编写,无需转换为outfile.cjs。
模板增量覆盖命令行参数解析:简化版本的vue-cli commander,预设默认参数,如使用预设可跳过问题询问,自动拉取对应模板。
问题答案统计:prompts收集问题答案,输出成对象形式,与vue-cli中的inquirer功能类似。
颜色渐变:utils\banner.js中实现终端输出的美丽颜色渐变功能。
文字颜色格式化:kolorist库,将颜色注入输入/输出,相当于vue-cli中的chalk。
pinia:更简洁的状态管理方案。
vitest:详细信息见相关文章。
git submodule:常规操作,playground文件夹即为一个submodule。
js语法书写shell:以js形式编写shell脚本,例如scripts\snapshot.mjs需先执行npm run build。
pnpm:自行搜索了解。
husky7:git hooks相关。
npm-run-all:自行搜索了解。
cypress:自行搜索了解。