Hilshire / blog

temporary blog
2 stars 0 forks source link

项目重构日记 #4

Open Hilshire opened 5 years ago

Hilshire commented 5 years ago

刚进公司写的项目竟然迭代了两年,是该重构一下升个级了。开个贴记录一下吧。

eslint-plugin-html -> eslint-plugin-vue

V2版本的 vue-cli 使用的还是 eslint-plugin-html,之后才升级成 eslint-plugin-vue 升级了 eslint 版本之后,较低版本的 eslint-plugin-html 报错了 image

详见 eslint-plugin-html error: It seems that eslint is not loaded. Compatibility with ESLint 4

Hilshire commented 5 years ago

尝试抽取业务组件

想把不同项目可以共用的组件抽取出来,发到公司的npm镜像上。能发包的组件,和项目里的业务组件,要求本身就不一样——不如说正确的发包本身就是一种要求。

抽象的方式是建立一个项目提供一个基本的壳然后一点点放进去。没什么太大意义但是玩一玩还不错。

rollup 和 babel

按照Publishing NPM package with Rollup, Babel, Flow, Jest and ESLint 进行了配置然后发现测试代码没有被编译。测试代码:

let a = 1;
new Promise((resolve, reject) => {
    setTimeout(() => resolve(a), 0);
})

顺便一体如果没用到 a 这边变量,上面的赋值语句会被处理掉。

然后发现这篇文章已经有一点过时了,babel 现在把包放在 @babel 域下,所以安装的包也发生了变化。 babel-core -> @babel/core babel-preset-env -> @babel/preset-env 修改之后顺利编译,但是Promise没编译,可能要手动加垫片。不过好麻烦,先放着吧。 最后的结果:

var a = 1;
new Promise(function (resolve, reject) {
  setTimeout(function () {
    return resolve(a);
  }, 0);
});
//# sourceMappingURL=index.esm.js.map

@babel/plugin-transform-runtime 和 @babel/plugin-external-helpers

稍微有点规模的 rollup 配置一般都能见到这两个东西,那么它们是什么呢? 其实官网上就有介绍 @babel/plugin-transform-runtime 它干了两件事:

不过看上面的东西就要搞懂做了什么事很难的,所以找了另一篇文章: 一口(很长的)气了解 babel 这样就差不多了 还想了解就再看这个: 你真的会用 babel 吗? 羡慕大佬的博客

要注意 @babel/plugin-transform-runtime 是运行时使用的依赖,对应的@babel/runtime 需要放在 dependencies 而不是 devDependencies 中(这一条存疑,希望有人能指教一下)

做好之后 Promise 会变成从 corejs 中引入的模块 import _Promise from '@babel/runtime-corejs2/core-js/promise';

之后发现这样配出来的 rollup 是不能编译 requrie 的。

emm... 我选择webpack

依赖问题

项目使用的是 iview@2.5.3,由这个项目抽出来的业务组件也必然依耐 iview

如果按照通常的方式,让项目依赖 iview, 会导致如果要使用组件库,就要安装 iview . 如果版本相同还好说,版本不同就会单独安装一个不同版本的 iview,这显然是不可接受的。做成这样的话就无法对外提供服务了。

不过 npm 管理依赖的方式很全面。除了开发常见的 dependenciesdevDependencies 之外,还有 peerDependencies

dependencies: 指定项目运行时所依赖的模块 devDependencies: 指定项目开发时所需要的模块 peerDependencies:指定当前模块所在的宿主环境所需要的模块及其版本 谈谈npm依赖管理

更全面的包管理可以看这里:你需要知道的几类npm依赖包管理

可以看一下 html-webpack-pluginpackage.json ,可以看到它就是用 peerDependencies 指定了插件依赖 webpack 的。安装这个插件不会安装 webpack ,但是如果你的项目中没有 webpack 会导致报错。

image

看一下 package.json文档,大部分人都会惊讶 package.json 提供了这么多选项。

发布轶事一则

0.0.1 版本完成之后,理所当然的要发到公司的 npm 镜像上。然而公司的 npm 镜像功能一直奉欠,这让我十分难受。我那个组件库名字叫 canon ,这确实是一个很容易想到的名字, npm 上也确实有一个包叫 canon

一般来说,这样的包是发不上去的,我是这么想的。然而实际上发布成功了,我上镜像上一查,就找到了那个同名的包。镜像把我的推送合并到那个包里去了。

image

如果 canon 的作者看到,必然会满脸问号吧。

于是给自己的包加了个域。之前的 gitlab 群组取名非常随意,唤作: frostmourne 。于是,公司 npm 镜像的域里面多了一个 frostmourne scope,里面有一个叫 canon 的项目。

嗯, Lich king 想不到还是个音乐爱好者哩。

关于 scope 相关在这里可以查到: npm - 参考手册

回归 rollup

在使用 webpack 之后,项目变得顺风顺水起来。然而,当我把包发到 npm镜像之后,有一个问题等待着我。 export default 的对象没有被引入,引入的是一个空对象。

// canon/src/index.js
export default {
    version: '0.0.1',
    install,
    ...
}
// test-canon-pkg/src/main.js
import canon from '@frostmourne/canon';
console.log(canon); // {}

在网上搜索了一下,得到了这个解释:https://stackoverflow.com/questions/44541561/webpack-output-is-empty-object

于是按照上面的修改配置,成功解决问题。

...
    output: {
        path: path.resolve(__dirname, '..', 'dist'),
        filename: 'index.js',
        library: 'canon',
        libraryTarget: 'umd',
    },
...

之前一直在想为什么写轮子的时候偏向 rollup 呢?原因很简单,它从一开始的出发点就是这个!而 webpack 的出发点是 web app ,这导致两者的侧重点不同。

比如对 webpack 来说 code split 是最重要的功能之一,以至于在 webpack4 直接从插件集成到配置里。而对 rollup ,把代码打包成单个文件时合适的。这些细节会一点点体现出来。

这样一来,这个项目应该rollup 打包。我还得把它请回来。

不过也不会 remove webpack 。和众多组件库一样,canon 有自己的介绍页。这一部分需要 webpack

工具本身并不重要,重要的是在合适的场合使用合适的“锤子”。

第一次 pr

然后被拒了。这pr确实比较水,也算是意料之中。不过即使只是提一个pr也是有门槛的。

pr是一个工作流的问题。这里有两篇不错的文章,而我不打算再出篇幅说这个。其中的 git 操作本身就是开发应该掌握的知识,每一步的原因也很明了。和常见的工作流相比,多了一步 forkpr 本身而已。而解释其后工作流相关内容,已超出我的能力。我对工作流并无深刻的见解,恐怕不能写的比这两篇文章更好。所在团队的项目也不太可能基于同样的工作流。

不过其它部门也确实有使用 pr 的项目,我至今才知道其中的意义,而项目的建立者时候并没有向其它人科普的打算。对于老师,也要有学生才行吧。

GIT团队合作探讨之二--Pull Request 3.3 创建 Pull Request

成果

image 项目中独立于业务的公共组件单独提取成了一个 npm package,之后有适合做成公共组件的就在 canon 里迭代。业务组件与项目分开,其它项目可以方便的引用。

同时,团队成员在写组件时要考虑之后可能会放到 canon 之中,组件变得更加纯粹。

而 canon 本身的发展也可以促进团队的进步,如何写好一个组件库是一个很讲究的事情。关于canon,也许写在另一个commet里比较好。

image

Hilshire commented 5 years ago

升级webpack

花了两天把项目代码按照公司的 eslint 规范犁了一边...想死。码农真是名不虚传啊。如果不是 eslint 可以帮你修复大部分的格式问题,感觉要升仙了。

于是目前在着手把 webpack2 迁移到 webpack4

遇到的第一个问题,怎么用 npm 把 依赖升级到最新版本。

看了下文档,没找到答案不过看到很有趣的事情。

As of npm@2.6.1, the npm update will only inspect top-level packages. Prior versions of npm would also recursively inspect all dependencies. To get the old behavior, use npm --depth 9999 update.

不知道升级最外层的时候外层依赖问题是怎么解决的。

然后文档没有给我答案,不过看到那个 latest 想起来了该怎么做。就是一遍遍敲 latest 手有点疼。而且会给我安装 beta 版本,略不安。

分支处理

目前是这样:

可以想见之后解决冲突抓耳捞腮的样子了

updatre list

webpack 相关

从错误信息来看应该是不能解析 css 部分了。增加 VueLoaderPlugin 后解决。参考了这篇文章: ac undefined A We - Config from Scratch For Vue

css-loader

删除了 minimize 选项,新版本的 css-loader 已经不支持了,不知道现在是怎么个压缩法。 之后应该会再加上 extract-loader,不过当务之急是让项目能正常跑起来。 加个 TODO

一个诡异的bug

升级过程中最难解决的问题,大致如下:

Module parse failed: 'import' and 'export' may only appear at the top level. You may need an appropriate loader to handle this file type.

之前也说过项目中有动态导入,也加了对应的 babel 插件,但是依然报错。结果这居然是一个 npm bug.

详见:Parsing of import() fails in 4.29.0 (Compilation issue, related to dynamic import)

改用 yran 装了下依赖就解决了。

另一个诡异的bug

最难解决问题的有力竞争者

Tapable.plugin is deprecated. Use new API on .hooks instead

按照上面说的升级 extract-text-webpack-plugin ,没有解决。

升级了所有 webpack 相关的插件,没有解决。

最后发现 dev-server 里就用到了 plugin。这时候,我们开始理解 webapck 编译 API

compiler-hooks

这里贴一张图,可以感受一下 来自https://github.com/jantimon/html-webpack-plugin/issues/1054#issuecomment-424297878 show html-webpack-plugin-after-emit error

vue-cli 2 中的代码改成了这样:

compiler.hooks.compilation.tap('compilation', function (compilation) {
  compilation.hooks.htmlWebpackPluginAfterEmit.tapAsync('html-webpack-plugin-after-emit', function (data, cb) {
    hotMiddleware.publish({ action: 'reload' })
    cb()
  })
})

但是还是不行。首先打出来的包就不一样,而且hooks没有这些值。到此我已经画了一天时间,和计划的时间有出入了。我必须寻求简单一点的方法。

于是决定用 vue-cli 搞一个样板项目然后把 src 塞进去,希望这次可以顺利

升级了...吗?

无法解决中间件的问题,我尝试使用 webpack-dev-server,然后就...成功了。

但是在尝试使用 webpack 打包时犯了难。webpack 执行,然后没有结果。没有 output,没有结束,程序就挂在那儿。我倒是可以挂机一晚上看它是不是真的在打包,不过最终没有选择这么做。

写了一个最简单的 production config,除了 modecss loader 外什么都没有,结果成功了。

最终的数据,打包时间:66057ms -> 113081ms

???

因为 product 为空了,之前配置的优化项也没了,比如某 dll ,又比如某parallel-uglify 插件,不过时间加了一倍还是有点问题吧。

不过体积倒是小了不少,12.1M -> 8.77M

不过还是有一种花费这么长时间干了什么的感觉...

5.16 更新:重新打包了一次时间大致差不多,看来每次打包时间波动也是有的。总算有动力继续下去了。

更新 webpack.prod.conf.js

之前用的是 prod mode 的默认配置,打包出来的结果和之前并不相同。使用原先的 webpack.prod.conf.js 后,报错。

image

原因是 utils 里的 generateLoaders 使用了 extract-text-webpack-plugin .

Since webpack v4 the extract-text-webpack-plugin should not be used for css. Use mini-css-extract-plugin instead. vue-loader 里有了新的方案:

npm install -D mini-css-extract-plugin
// webpack.config.js
var MiniCssExtractPlugin = require('mini-css-extract-plugin')

module.exports = {
  // 其它选项...
  module: {
    rules: [
      // ... 忽略其它规则
      {
        test: /\.css$/,
        use: [
          process.env.NODE_ENV !== 'production'
            ? 'vue-style-loader'
            : MiniCssExtractPlugin.loader,
          'css-loader'
        ]
      }
    ]
  },
  plugins: [
    // ... 忽略 vue-loader 插件
    new MiniCssExtractPlugin({
      filename: 'style.css'
    })
  ]
}

删除 htmlWebpackPlugin 里的 chunksSortMode: 'dependency'webpack4 里不再需要了。

https://github.com/jantimon/html-webpack-plugin/issues/1055#issuecomment-424605059

成果

搞定了——虽然这么说,其实开发体验变化不是很大。当然,打包出来的体积实实在在变小了。另外,从 webpack-bundle-analyzer ,打出来的包有相当程度的优化。考虑到项目是从 webpack2 迁移过来,也是理所当然吧。

Hilshire commented 5 years ago

自动化脚本

这个项目稍微有点历史,带来的结果就是它没有前后端完全分离。每次打包之后需要手动复制粘贴到后端的目录里面去,非常麻烦了。所以想写一个脚本处理这个事情,希望能做到这样:

打包时自动更新后端代码,然后把打出来的文件复制到后端仓库的地址里,然后 push

旧版本的 vue-cli 的结构还是比较简单,build 就是跑一个 js 脚本,它会删除静态文件,并通过 ora, chalk 提供更友好的终端。既然有了这么一个位置,我就不客气的那来用了。

个人对 node 不是很熟悉,再次捡起来 error-first 写法着实恶心到我了。询问组里某大神,才知道 node 现在提供了 util.promisify(original). 为什么不改 api 风格呢... 这是绝对的痛点吧。

为了这一块的功能,用到了以下 pkg

这些是原先就有的,同时又加上了

本以为是个简单的工作,结果出人意料的麻烦,似乎又能在上面折腾不少时间

Hilshire commented 5 years ago

commit hook

其实并不是我做的,而是和另一位大佬商议了一下,他顺手就做了。

我不是很清楚 vue-cli3 是什么样,如过是用 vue-cli2 的项目的话,违反 eslint 规则会造成编译失败,并在客户端上显示一个友好的错误提示。其中,提示显示在客户端的设置在这里:

module.exports = {
  //...
  devServer: {
    overlay: {
      warnings: true,
      errors: true
    }
  }
};

这种做法还不错,但是在开发中频繁触发 eslint 的感觉不是很好,尤其很多错误是可以用 --fix 自动处理掉的。一种观点是 eslint 不应该阻碍开发,有很多人在反思如何更好的使用 eslint. 有一种实践是这样:

当然, vue-cli 不可能默认使用它的开发团队都配置了 git hook;另一方面,想使用 eslint hook 团队已经具备了工程化意识,并不需要 vue-cli 去做额外的事情了。

一开始的时候,我打算简单的更新一下 git hook Git Hooks

而我的同事比我走的深一点。他加了两个依赖: husky lint-staged

其中,husky 是一种可以友好的处理 hook 处理的工具, lint-staged 可以只检查提交的内容。这已经是一套非常成熟的方案了。实际上,当我着手做这件事的时候,深感我司的前端已经远远落后于大部队了。

Hilshire commented 5 years ago

重构和工程化

有的朋友可能会觉得奇怪:明明标题是重构,怎么看内容就一点一点往工程化偏移了?

这里面的原因有二:

  1. 代码重构很多时候是在帮前人擦屁股,具体业务值得记录的不多
  2. 对一些页面进行重构之后发现虽然做了很多工作,效果却不是很明显。页面级的重构,如果没有维护相关页面,那么几乎不会有产出。相对的,如果之后要维护了,在那时进行重构也不是不可以。

所以,一段时间之后,工作中心一点点偏向了工程化。当然重构一直有在做,只是更多的会在项目整体上做一些调整,这是可以直接起效的。

工程化有一个很大的不同:普通的业务由产品给你提出需求,你实现他的功能就好。而产品的需求往往很具体。而工程化方面需求需要自己想,做了 hook, CI 都要自己寻找什么样的方案适合你的团队。对我而言这是另一个意义上的挑战。

gitlab runner

参考了这些: 用 GitLab CI 进行持续集成 Docker搭建自己的Gitlab CI Runner 劈荆斩棘:Gitlab 部署 CI 持续集成

虽然不知道是否正确,我把 runner 理解为 CI 服务器。runner 的安装大致分为以下步骤:

在register的时候会让你选择 runner 的类型,很多实例都是 docker 。其实如果不熟悉的话(比如我),选择 shell 会更合适

其中 runner 需要和 gitlab 版本对应。我司的 gitlab 版本为 GitLab Community 8.17.4,已经大幅落后,使用官网上最新的 runner 会报错。启动之后会在 CI 页面上出现对应的 runner。

image

测试用流水线如下:

# 定义 stages
stages:
  - build
  - test
# 定义 job
job1:
  tags: [bd_tag]
  stage: test
  script:
    - echo "I am job1"
    - echo "I am in test stage"
# 定义 job
job2:
  tags: [bd_tag]
  stage: build
  script:
    - echo "I am job2"
    - echo "I am in build stage"

注意其中的 tags,需要和上图的 runner 对应。准确来说,需要和已有的 runnertag 对应,如果没有找到对应的 runner 会因此而 pending

然而,这样简单的测试依然报错了 image 有时真会让人觉得不如想办法装一个 jenkins 算了(哭笑)

待补充...

Hilshire commented 5 years ago

项目重新开始优化打包等问题了,webpack 相关单独写 issue 记录了 #24