element-plus 代码一直在变, 找一个较旧的commit用于学习
旧的commit写的有点绕,还是用最新的吧😩
仓库地址 github
一 使用 pnpm 初始化 monorepo 环境
本地创建项目学习时,最好不要叫@element-plus, 因为 pnpm install @element-plus/components -w 会安装远程的,而不是本地
-
只发布packages文件夹下的 packages 所以设置 private:true
-
packages 下的所有包都需要初始化
cd packages/components && pnpm init -y{ "name": "@element-plus/components", "main": "index.ts"`, }
QA:
-
.npmrc 中 shamefully-hoist = true 作用?
使得依赖安装到 node_modules,因为 pnpm 默认使用软链来节省内存
-
typings 文件目的 ?
.ts 文件中,import xxx form 'x.vue' 时,ts 报错:找不到模块“./app.vue”或其相应的类型声明, 会去当前项目查找 .d.ts 文件
-
运行 play 项目时, 不想 cd play,而是在根目录下运行,如何设置?
- play/package.json 中 具有 scripts:dev:vite
- 根 package.json 中添加 pnpm -C play dev
-
希望各 package 可以相互引用?
- cd 到根目录
- pnpm install @element-plus/components -w
- 此时,检查 node_modules 目录, 应该出现@element-plus 软链接
常见错误
- Preprocessor dependency "sass" not found. Did you install it?
pnpm install sass -w -D
二 gulp + rollup 打包
-
pnpm install gulp @types/gulp sucrase -w -D- 默认只能使用 gulpfile.js,为了支持 gulpfile.ts,需要使用Sucrase, 下载安装即可,无需显式引用
-
添加打包命令
"build": "gulp -f build/gulpfile.ts" -
需要打包出哪些产物?
- /dist:
- umd全量包(全部整合到一个js文件里): /dist/index.full.js
- esm全量包(全部整合到一个js文件里): /dist/index.full.mjs
- .css全量包
- /es:
- esm包, 多个目录,有一个 index.js 作为出口,以及 .d.ts文件
- /lib:
- cmd包, 多个目录,有一个 index.js 作为出口,以及 .d.ts文件
- /theme-chalk
- css: 多个.css文件, 直接扁平放到 /theme-chalk
- scss: scss源文件, 放在/theme-chalk/src目录下
- ../types
- 所有包的声明文件 @types , 为了给umd包使用
-
如何打包?
使用gulp控制整个构建流程
import { series, parallel } from 'gulp' series( withTaskName('clean', () => run('pnpm run clean')), parallel( runTask('buildModules'), // 构建 /es 和 /lib runTask('buildFullBundle'), // 构建 /dist runTask('generateTypesDefinitions'), // 构建 @types 声明文件 runTask('buildHelper'), series( // 构建 /theme-chalk withTaskName('buildThemeChalk', () => run('pnpm run -C packages/theme-chalk build') ), copyFullStyle ) ), parallel(copyTypesDefinitions, copyFiles) )ps:
pnpm run --filter ./packages --parallel --stream build会寻找 ./packages 下的子包的 build 命令, build 调用 gulp,gulp 寻找 gulpfile.js, gulpfile.js 里实现针对该包的打包细节
打包 esm 和 commonjs
要输出 esm, cjs 等模块类型, 因此这一块使用 rollup + esbuild 来打包
import commonjs from "@rollup/plugin-commonjs" // 将 CommonJS 转换成 ES2015 模块,应该在其他插件之前
import nodeResolve from "@rollup/plugin-node-resolve" // 识别 import,require 后面的模块名称, 补全.ts,.json 等后缀,并且返回文件路径给rollup
// 入口配置
const inputOptions = {
input,
// 插件有执行顺序要求,先左后右
plugins: [
nodeResolve(),
commonjs(),
// .vue文件转成 .js 文件; script = defineComponent(),script.render = parse(template)
vue({
target: "browser",
}),
esbuild({ // rollup 只支持js文件内容, 识别ts内容并转成js
target: "es2018",
}),
],
// Rollup 将把所有 imports 的模块打包在一起
external: generateExternal(),
}
// 出口配置
{
/** ems */
format: "esm",
// 多出口时, 必须指定输出的目录
dir: path.resolve(projRoot, "dist/element-plus", "es"),
exports: "auto", // default
preserveModules: true,
preserveModulesRoot: path.resolve(projRoot, "packages"),
sourcemap: true,
entryFileNames: `[name].mjs`,
},
处理模块时, rollup 会查找模块并尝试解析模块内容
比如, 读取到 components/icon/style/index.ts 文件时:
// @file components/icon/style/index.ts
import "@y-element-plus/theme-chalk/src/icon.scss"
会去查找@y-element-plus/theme-chalk/src/icon.scss, 并调用相关的插件,解析scss文件,在这里, 因为我们在 rollup 里没有配置任何scss相关的插件,所以无法解析 @use "mixins/mixins" as *;
Error: Unexpected character '@' (Note that you need plugins to import files that are not JavaScript)
把 .scss 的处理放在 /theme-chalk/gulpfile.ts 里处理 , 所以这里的 rollup 不用来处理样式, 也就不需要配置scss相关插件,那怎么让rollup 跳过这个文件呢?
@y-element-plus替换为y-element-plus- 把
y-element-plus/theme-chalk/**定义成外部模块
// 修改 `@y-element-plus` 为 `y-element-plus`
export function ElementPlusAlias(): Plugin {
const THEME_CHALK = `${EP_PREFIX}/theme-chalk`
return {
name: 'element-plus-alias-plugin',
resolveId(id, importer, options) {
if (!id.startsWith(EP_PREFIX)) return
if (id.startsWith(THEME_CHALK)) {
return {
id: id.replaceAll(THEME_CHALK, `${EP_PKG}/theme-chalk`),
external: 'absolute',
}
}
return this.resolve(id, importer, { skipSelf: true, ...options })
},
}
}
// 定义成外部模块
export const generateExternal = async () => {
return (id: string) => {
const packages: string[] = ['vue']
packages.push('element-plus/theme-chalk')
// dependencies
packages.push('@vue', ...getPackageDependencies(epPackage))
}
return [...new Set(packages)].some(
(pkg) => id === pkg || id.startsWith(`${pkg}/`)
)
}
}
编译后变成:
import 'y-element-plus/theme-chalk/src/icon.scss'
对于其他的 @y-element-plus 导入, rollup 内部会转成相对路径, 无论是否开启outputOptions.preserveModules
import { withInstall } from "@y-element-plus/utils/with-install"
export default withInstall
import { withInstall } from './with-install.mjs';
export { withInstall as default } from './with-install.mjs';
//# sourceMappingURL=index3.mjs.map
如果无效, 检查nodeResolve的配置是否包含.ts:
nodeResolve({
extensions: [".ts"],
}),
inputOptions.input
使用多入口,
outputOptions.preserveModules
默认:false, 扁平放置到出口目录,并且依赖模块会被内联到文件中
如:
// @file packages/components/base/index.ts
import lodash from "lodash"
console.log(lodash.debounce)
编译为:
true: 让他保持与入口目录 packages一样的目录结构,并且依赖模块会变成相对路径
import lodash from '../../../node_modules/.pnpm/lodash@4.17.21/node_modules/lodash/lodash.mjs';
console.log(lodash.debounce);
//# sourceMappingURL=index.mjs.map
多入口时, 一般都想要保留目录结构, 但是lodash 意外引用了 node_modules, 这不是我们期望的, 我们期望的是 import lodash from 'lodash, 解决办法是 把lodash添加为外部模块
inputOptions.external
使用external来把 一些模块设置为外部模块
代码里不止会用到 lodash, 还会用到其他包, 如何确定该 external 哪些包? 根据经验, import 的模块一般来自 package.json 中的dependencies和peerDependencies字段, 所以就有
const {dependencies,peerDependencies} = await import('package.json')
external = [...new Set(dependencies,peerDependencies)]
当然也有例外,如 element-plus 使用了 import { isFunction } form @vue/shared, @vue/shared 包没有显式出现在 package.json 文件中, 他是通过: vue -> @vue/runtime-core -> @vue/shared来的, 那么这个库也是需要加进去的
总结: preserveModules 为true会将模块解析为相对路径, 为false会内联模块
outputOptions.preserveModulesRoot
某些时候, 入口的文件目录也不是我们想要的,可以使用 preserveModulesRoot 调整目录结构, 大致意思是
outDir = [...outDir, ...preserveModulesRoot]
以此可以减少 preserveModulesRoot 这个目录层级
打包 Dist 全量包
dist下的包:
- 一个全量样式的 index.css
- 一个全量js的umd包 index.full.js
- 一个全量js的esm包 index.full.mjs
对于index.css, 在theme-chalk里处理, 然后把处理结果拷贝到该目录下即可
根据 esbuild 的启发:
请注意打包与文件连接不同。在启用打包时向 esbuild 传递多个输入文件 将创建两个单独的 bundle 而不是将输入文件连接在一起。 为了使用 esbuild 将一系列文件打包在一起, 在一个入口起点文件中引入所有文件, 然后就像打包一个文件那样将它们打包。
思路: 构建这样一个入口 js, 又由于packages下的所有内容都是目录, 因此创建一个 叫 element-plus 的目录, 在 element-plus/index.ts 里引用所有模块即可
打包样式
-
样式全部在.scss 文件, .scss 文件全部在 them-chalk 文件夹里, components 下的组件不支持使用书写样式
-
存在两种引用方式
- 全量引用
import "element-plus/dist/index.css";
- 按需引用
// vite.config.ts
import AutoImport from "unplugin-auto-import/vite";
import Components from "unplugin-vue-components/vite";
import { ElementPlusResolver } from "unplugin-vue-components/resolvers";
export default {
plugins: [
// ...
AutoImport({
resolvers: [ElementPlusResolver()],
}),
Components({
resolvers: [ElementPlusResolver()],
}),
],
};
所以 build 后, 这两个地方需要有样式代码
全量: element-plus/dist/index.css,
按需: element-plus/es/components/icon/style/css
按需的路径参考 [unplugin-vue-components/resolvers](element-plus/es/components github.com/antfu/unplu…) 源码而来
// @filename: element-plus/es/components/icon/style/css.ts
import "@element-plus/components/base/style/css"; // 服务端渲染时,不加载这个文件
import "@element-plus/theme-chalk/el-icon.css";
// element-plus/es/components/icon/style/index.ts
import "@element-plus/components/base/style";
import "@element-plus/theme-chalk/src/icon.scss";
// @element-plus/components/base/style/css.ts
import "@element-plus/theme-chalk/base.css";
// @element-plus/components/base/style/index.ts
import "@element-plus/theme-chalk/src/base.scss";
生成声明文件
解决两件事:
- 分别编译
.vue和.ts文件 packages/element-plus文件夹下编译为@types的直接子元素
ts-morph 配置:
const project = new Project({
compilerOptions: {
emitDeclarationOnly: true,
outDir: resolve(projRoot, "dist/types"),
baseUrl: projRoot,
// 不要忘记加 /*
paths: {
"@y-element-plus/*": ["packages/*"],
},
},
tsConfigFilePath: resolve(projRoot, "tsconfig.json"),
skipAddingFilesFromTsConfig: true,
})
非.vue文件, 编译以后有两个问题:
- scss文件也被加入进来了,需要排除
- @y-element-plus 前缀没有像rollup那样被自动处理, 需要收到替换
需要手动处理
// 获取输出文件
sourceFiles.map(async (sourceFile) => {
sourceFile.getEmitOutput().getOutputFiles()
const tasks = emitFiles.map(async (outputFile) => {
const filepath = outputFile.getFilePath()
await fs.mkdir(path.dirname(filepath), {
recursive: true,
})
// 替换前缀
await fs.writeFile(
filepath,
outputFile.getText()
.replaceAll("@y-element-plus/", "y-element-plus/es/")
.replaceAll('@y-element-plus/theme-chalk','element-plus/theme-chalk'),
"utf8"
)
})
})
.vue 文件, 使用glob 读取所有文件, 借助vue/compiler-sfc 获取 script 标签里的内容, 然后使用project.createSourceFile 添加为 sourceFile,然后重复以上步骤, 替换前缀