Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

「大前端」尚妆达人店 UI 组件化 工程实践(weex vue) #37

Open
ShowJoy-com opened this issue Dec 2, 2017 · 6 comments
Labels

Comments

@ShowJoy-com
Copy link
Owner

ShowJoy-com commented Dec 2, 2017

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

发表于尚妆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文件下载

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

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

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

3、5.4.2版本后 npm/npm#17979

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

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

总结

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

@Uheinanba
Copy link

spon-ui没有开源吗

@Eden-Harris
Copy link

怎么找不到相关文档啊

@ShowJoy-com
Copy link
Owner Author

@Uheinanba @Eden-Harris spon-ui 组件库目前还在闭源开发 这次分享我们单独维护组件的一些实践

阿里的飞猪 有开源一款 weex的ui组件库 开源看看 https://alibaba.github.io/weex-ui/#/

@yuandyu
Copy link

yuandyu commented Dec 4, 2017

什么时候开源啊

@CodeDreamfy
Copy link

CodeDreamfy commented Aug 21, 2018

请问如何做到 组件的异步加载,我使用了类似于:

components: {
    'Title': resolve => require(['@/components/big/title.vue'], resolve)
}

这种方式加载,但是打包出来的chunk里面包含了window["webpackJsonp"]这种东西,导致Android或者ios不识别,目前暂时还是使用的:is的方式,但是不希望一次性将所有组件都加载进来~

以上,期待您的解答

@vegawong
Copy link

vegawong commented Sep 3, 2018

lerna 发布到内部npm仓库遇到一个问题 貌似是lerna执行了npm access指令, 私有仓库没有支持, http请求404的字样? 请问有遇到过吗?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

6 participants