「前端」尚妆 UI 组件库工程实践(weex vue)

2023-06-13,,

本文来自尚妆前端团队南洋

发表于尚妆github博客,欢迎订阅!

前言

尚妆大前端团队使用 weex 进行三端统一开发有一段时间了,截止本文发表「达人店」APP大部分页面都已经用 weex 进行了重构,在此期间也积累了一些基础组件和业务组件。

之前维护组件的方式是在达人店项目的工程内维护一个 components 文件夹,随日常开发迭代,并行需求与开发人员的增多,这种维护方式也暴露出一些问题。

1、开发人员可以随意跟随需求开发修改 components 内的组件,破坏约定好的规范,或埋入 bug。

2、定义组件缺少规范,比如在某个需求开发中, A 开发人员觉得这个功能可以抽离成组件,就直接在 components 内定义并使用,但实际却是伪需求,用了一次就再也没有人使用,造成 components 组件库的部分冗余。

3、组件抽离过程无法协同使用,比如 A 开发同学切了个特性分支 feature/A,并根据项目抽了个通用组件 ComponentA,B 开发切了个特性分支 B,也想使用这个 ComponentA 组件,但此时两人在不同分支,代码并不能共享。

4、。。。

基于上述不便之处,我们尝试将 components 抽离出来,放到内部私有 npm 仓库中以 npm 包的形式去维护。

也就是我们将 spon-ui(内部组件库名称)作为单独的一个项目去维护,加以约束形成组件库开发规范,能有效的解决上述问题。

此文就是此次抽离过程的一些实践,包含了组件的调试文档调试npm使用组件 发布等内容。当然 weex 的语法同 vue,这些实践也同样适用于 vue。

1、组件库的调试

先看下 spon-ui 组件库项目的目录结构。

|- spon-ui
||-- build
||-- docs
||-- examples
||-- packages
|||--- weex-field
||||---- index.js
||||---- field.vue
||||---- example.vue
||||---- readme.md
||||---- package.json
||-- src

build 中存放一些脚本执行文件,用于工程的调试、发布。
docs 中存放文档调试的脚本,生成一个文档调试服务器。
examples 中存放组件调试的脚本,生成一个组件调试服务器。(不存放组件例子)
packages 存放真实组件,以及组件的文档和例子。
src 存放组件可以使用的公共方法。

组件的调试

examples 文件夹内就是组件调试的相关脚本,这个文件夹在组建开发过程中是不需要变动的,只是定义了调试服务器的一些逻辑。并不包含真实的组件例子。

而真实的例子存放在相应组件目录下,example.vue 中引入当前目录下的 vue 组件,调试时是针对 example.vue 进行调试,因为调试组件需要模拟使用组件的场景(改变传入值,用户交互等)。

当执行 npm run dev:components 时,开发同学会看到浏览器打开页面:

选择想要调试的组件,比如说 weex-dialog ,进入到 weex-dialog 的调试界面。

开发同学此时修改 packages 目录中的 weex-dialog 的组件内容,会实时看到修改内容,进行调试。

console 中输出二维码

另外我们开发的组件是基于 weex 的,意味着开发的组件需要支持三端(iOS android H5),所以在 console 中会打印当前组件js的二维码,用于 native 调试。

如何在console中输出二维码也是个小trick,首先利用js的二维码库将资源生成二维码图,然后利用console输出背景图的机制打印二维码。

console.log("%c", "padding:75px 80px 75px;line-height:160px;background:url(" + base64 + ") no-repeat;background-size:160px");

整个调试页面是通过单页面的形式展现的,使用 vue-router 进行路由控制,weex 也支持 vue-router ,所以这个单页面在 native 中也能良好运行。

自动生成组件相关信息

在每次执行 npm run dev:components 命令时,会根据 packages 目录下的组件自动生成 nav-list.js 文件,这个索引文件用来定义 vue-router 的路由信息,以及调试主页的组件列表。这样做可以完全将调试过程抽离成黑盒,开发人员只需关注 packages 目录下的开发即可。

const routes = navList.map((item) => {
const path = item.path;
return {
path,
// 需要加vue后缀,不然webpack会将examples下的所有文件都require一下
component: require('examples/' + item.exampleRequire + '.vue'),
};
});
routes.push({
path: '/',
component: require('./app.vue'),
})

// 组件列表也通过 nav-list.js 渲染
<spon-cell-group>
<spon-cell
v-for="(page, jndex) in item.list"
:key="jndex"
:title="page.title"
:is-link="true"
@click="changePage(page)"
></spon-cell>
</spon-cell-group>

webpack require 动态的资源

本文使用 webpack 3.x.x

上节提到的 require 动态的模块时,如果不表明文件类型,webpack会将该目录下所有资源都 require 一遍,造成的问题是如果目录下有某类型的文件,而又没有使用对应的loader,在编译过程就会报错。上节中如果不加 .vue 后缀, webpack会将 examples 目录下所有资源都require一遍。

所以在定义各路由的component时,需要加上 vue 后缀,查找vue文件。

component: require('examples/' + item.exampleRequire + '.vue'),
};

webpack的文档说明在 https://webpack.js.org/guides/dependency-management/#require-context

在 webpack 的官方文档里列出了动态 require 的原理,对于 require("./template/" + name + ".ejs"); 含表达式的引用,webpack 解析此处的 require,得到两个信息:

1、 目录为 ./template
2、匹配规则为 /^.*\.ejs$/

然后 webpack 会根据这两个信息得到一个 context module,这个模块包含了 ./template 目录下所有以 .ejs 为后缀的模块。

{
"./table.ejs": 42,
"./table-row.ejs": 43,
"./directory/folder.ejs": 44
}

还有一个 require.context() 方法可以自定义动态引用的规则,文档中也有示例,官网给出了一个基于此的demo,引入一个目录中所有符合规则的模块。

function importAll (r) {
r.keys().forEach(r);
} importAll(require.context('../components/', true, /\.js$/));

文档的调试

组件开发的差不多了,就要编写相应的文档,方便同事小伙伴使用,执行 npm run dev:docs 会开启文档调试服务器,方便开发同学编写文档。

文档服务器的逻辑放在 docs 目录下,同样与组件代码解耦,左侧的组件信息动态取自 packages 目录下的组件信息,右侧的组件预览直接使用 examples 目录下的组件调试逻辑,中间的部分取自 组件中的 readme.md 文件。

整个文档应用也是基于 vue + vue-router 开发。

<div class="nav-bar-container">
<page-nav></page-nav>
</div> <div class="document-area-container markdown-body">
<router-view></router-view>
</div> <div class="mock-phone-container">
<page-preview :component-name="componentName"></page-preview>
</div>

<router-view> 就是对应的路由所展示的文档内容,相应的在定义路由信息时需要确定路由以及路由所对应的 readme.md 路径。

const routes = navList.map((item) => {
const path = item.path;
return {
path,
component: require('mds/' + item.mdRequire + '.md'),
};
}); const router = new VueRouter({
routes,
});

markdown 转换 vue

在引用组件时使用了 .md 后缀,这里是采用了 vue-markdown-loader 饿了么出品的loader。这个loader还是借助vue-loader,首先会将 md 的内容转换成 html ,然后再转换成 vue 所需要的单文件形式给vue-loader。

var renderVueTemplate = function(html, wrapper) {
// 本文作者注
// 传入的html是根据 markdown插件将md转换而来
var $ = cheerio.load(html, {
decodeEntities: false,
lowerCaseAttributeNames: false,
lowerCaseTags: false
}); ...
// 本文作者注
// 将html转换成 vue-loader 所需的字符串形式
result =
`<template><${wrapper}>` +
$.html() +
`</${wrapper}></template>\n` +
output.style +
'\n' +
output.script; return result;
};
var result =
'module.exports = require(' +
loaderUtils.stringifyRequest(
this,
'!!vue-loader!' +
markdownCompilerPath +
'?raw!' +
filePath +
(this.resourceQuery || '')
) +
');'; // 本文作者注
// 将转换好的字符串传给 vue-loader
return result;

2、基于 npm 脚本实现工程化

"scripts": {
"bootstrap": "npm i",
"dev:components": "node build/bin/dev-entry.js",
"dev:docs": "node build/bin/docs-dev-entry.js",
"build:docs": "node build/bin/docs-build.js",
"pub:docs": "npm run bootstrap && npm run clean && node build/bin/release.js",
"pub:components": "node build/bin/prepublish.js && lerna publish --skip-npm --skip-git && node build/bin/publish.js",
"clean": "rm -rf docs/dist && rm -rf docs/deploy",
"add": "node build/bin/add.js"
},

本项目中将所有常用的命令都进行了抽离,开发同学使用的命令最后暴露出4个:

npm run dev:components 组件的调试
npm run dev:docs 文档的调试
npm run pub:docs 文档的发布
npm run pub:components 组件的发布

推荐看阮一峰的博客 npm scripts 使用指南 ,将npm 脚本很细致的介绍了一遍。

自动生成脚手架

npm run add 会自动添加一个组件所需的脚手架信息,方便开发同学添加新组件。

这里推荐使用 json-templater/string 模块处理 string 模板的问题。

脚手架文件中的某些值会根据组件名的不同而不同,根据组件名自动生成对应的脚手架内容,更加方便开发。

npm link

组件在本地开发完成了,例子和文档都编写完毕,但不知在真实项目中使用会不会出现奇怪bug。

最原始的方法可以将组件复制到项目中的npm包中进行真实调试。

当然 npm 也提供了 方法专门解决这种问题。

1、首先在 spon-ui 组件库的根目录执行 npm link

2、回到项目目录,执行 npm link spon-ui ,两条命令就能将项目中原本引用的spon-ui 映射到本地的spon-ui目录中去。

3、npm unlink 取消软链。

3、源码依赖

上节提到的npm 脚本并没有提到组件打包的流程,因为如果在组件这层就进行打包,会增加一些webpack的冗余代码,增加字节,而且这个组件库目前完全属于内部项目使用,打包环境在项目中就存在,没有必要提前进行打包。

所以发布出去的组件包就是packages下的所有组件,项目中所依赖的都是组件的源码,称为源码依赖

要做到源码依赖,需要修改业务项目中(非本组件项目)的babel的配置。排除掉 spon-ui 组件

 module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules(?!\/.*(spon-ui).*)/,
loader: 'babel-loader',
options: {
cacheDirectory: true,
},
},
],
},

滴滴有篇webpack 应用编译优化之路有讲到源码依赖所带来的好处。

4、发布组件

我们使用了 lerna 来管理组件的发布,lerna 有两种发布方式,一种是一个项目的所有组件作为一个发布包,还有一种可以将一个项目中的多个组件分别发布。

我们使用了第一种,即所有组件统一成一个发布包。这种方式发挥不出 lerna 的威力,但是作为发布前的版本号管理还是不错的。未来如果要将各个组件单独发布,改一下配置就ok。

版本管理

测试版本的管理

在前文就提到过目前组件库的开发还是依赖于需求的迭代,小团队没有人专门开发组件,组件的开发会跟随需求的迭代而迭代。

那么在前期组件变更需求通过评审会后,就会跟随项目正式进入开发流程。项目开发会区分测试环境和预发全量环境,那么组件的版本号也需要区分测试环境和全量环境。

npm publish --tag

介绍一下 publish 的 tag,发布的 npm 包默认会有一个 latest 标签,每次执行 npm publish 都会自动将 tag 设置为 latest,也可以理解为稳定版,所以我们要做的是再添加一个 tag

npm publish --tag dev

这个命令代表添加一个名为 dev 的tag,并将此次发布的版本号贴上 dev 标签。

执行 npm dist-tag ls spon-ui 可以查看当前的标签所对应的版本号信息。

➜  spon-ui git:(master) npm dist-tag ls spon-ui
dev: 0.1.0-12
latest: 0.1.0

在项目中安装spon-ui的时候,根据情况分别执行

npm i spon-ui@dev
npm i spon-ui@latest

5、npm5 package-lock.json

组件发布完成了,就可以在项目中使用了,我们从npm3.x更新到了npm5,但是发现执行 npm i 时的现象跟网络上的科普文不太一致。

有提到不管怎么修改package.json文件,重复执行npm i,npm都会根据lock文件描述的版本信息进行下载。

也有提到重复npm i时,npm会不顾lock的信息,根据package.json中的包Semantic versioning 版本信息下载更新模块(lock貌似没啥用了)。

查阅资料得知,自npm 5.0版本发布以来,npm i的规则发生了三次变化。

1、npm 5.0.x 版本,不管package.json怎么变,npm i 时都会根据lock文件下载

https://github.com/npm/npm/issues/16866
这个 issue 控诉了这个问题,明明手动改了package.json,为啥不给我升级包!然后就导致了5.1.0的问题...

2、5.1.0版本后 npm install 会无视lock文件 去下载最新的npm

然后有人提了这个issue https://github.com/npm/npm/issues/17979
控诉这个问题,最后演变成5.4.2版本后的规则。

3、5.4.2版本后 https://github.com/npm/npm/issues/17979

大致意思是,如果改了package.json,且package.json和lock文件不同,那么执行npm i时npm会根据package中的版本号以及语义含义去下载最新的包,并更新至lock。

如果两者是同一状态,那么执行npm i都会根据lock下载,不会理会package实际包的版本是否有新。

总结

以上就是我们将UI组件从项目中迁移出来单独以npm包的形式去维护的实践过程,不完美还有待时间的考验,希望其中的一些内容能帮助到大家。

「前端」尚妆 UI 组件库工程实践(weex vue)的相关教程结束。

《「前端」尚妆 UI 组件库工程实践(weex vue).doc》

下载本文的Word格式文档,以方便收藏与打印。