组件化思考和团队组件规范

一、组件化思考

组件的存在的终极意义是为了复用,一个组件只要具备了被复用的条件,并且开始被复用,那么它的价值才开始产生。组件复用的次数越高、被传播的越广,其价值就越大。而要实现组件的价值最大化,需要考虑以下几点:

  • 1. 我要写一个什么组件?组件提供什么样的能力?
  • 2. 组件的适用范围是什么?某个具体业务系统内还是整个团队、公司或者社区?
  • 3. 组件的生产过程是否规范、健壮和值得信赖?
  • 4. 组件如何被开发者发现和认识?

二、组件化规范

一个优秀的组件除了拥有解决问题的价值,还应该具备以下三个特点:

  • 1. 生产和交付的规范性
  • 2. 优秀的质量和可靠性
  • 3. 较高的可用性

只有三者都能满足才可以称其为优秀组件,否则会给使用者带来各种各样的困惑:经常出Bug、坑很多、不稳定、文档太简单、不敢用等等。

2.1 目录结构

事实上,并没有一个官方的目录结构规范,但从耳熟能详的知名项目中进行统计和分析,可以得出一个社区优秀开发者达成非官方共识的一个目录结构清单:

├─ test         // 测试相关
├─ scripts      // 自定义的脚本
├─ docs         // 文档,通常文档较多,有多个md文档
├─ examples     // 可以运行的示例代码
├─ packages     // 要发布的npm包,一般用在一个仓库要发多个npm包的场景
├─ dist|build   // 代码分发的目录
├─ src|lib      // 源码目录
├─ bin          // 命令行脚本入口文件
├─ website|site // 官方网站相关代码,譬如antd、react
├─ benchmarks   // 性能测试相关
├─ types|typings// typescript的类型文件
├─ Readme.md    // 仓库介绍或者组件文档
└─ index.js     // 入口文件

以上目录清单是一个比较完整的清单,大多数组件只需要根据自己的需求选择性地使用一部分即可。一份几乎适用于所有组件的最小目录结构清单如下:

├─ test         // 测试相关
├─ src|lib      // 源码目录
├─ Readme.md    // 仓库介绍或者组件文档
└─ index.js     // 入口文件

2.2 配置文件

主要指的是各种工程化工具所依赖的本地化的配置文件,以及在Github上开源所需要声明的一些文件。一份比较全的配置文件清单如下:

├─ .circleci            // 目录。circleci持续集成相关文件
├─ .github              // 目录。github扩展配置文件存放目录
       ├─ CONTRIBUTING.md
       └─ ...
├─ .babelrc.js          // babel 编译配置
├─ .editorconfig        // 跨编辑器的代码风格统一
├─ .eslintignore        // 忽略eslint检测的文件清单
├─ .eslintrc.js         // eslint配置
├─ .gitignore           // git忽略清单
├─ .npmignore           // npm忽略清单
├─ .travis.yml          // travis持续集成配置文件
├─ .npmrc               // npm配置文件
├─ .prettierrc.json     // prettier代码美化插件的配置
├─ .gitpod.yml          // gitpod云端IDE的配置文件
├─ .codecov.yml         // codecov测试覆盖率配置文件
├─ LICENSE              // 开源协议声明
├─ CODE_OF_CONDUCT.md   // 贡献者行为准则
└─ ...                  // 其他更多配置

以上配置可以根据组件的实际情况,适用范围来进行删减。一份在各种场景都比较通用的清单如下:

├─ .babelrc.js          // babel 编译配置
├─ .editorconfig        // 跨编辑器的代码风格统一
├─ .eslintignore        // 忽略eslint检测的文件清单
├─ .eslintrc.js         // eslint配置
├─ .gitignore           // git忽略清单
├─ .npmignore           // npm忽略清单
├─ LICENSE              // 开源协议声明
└─ ...                  // 其他更多配置

上述清单移除了只有在Github上才用得到的配置,只关注仓库管理、发包管理、静态检查和编译这些基础性的配置,适用于团队内部、企业私有环境的组件开发。如果要在Github上维护,则还需要从大清单中继续挑选更多的基础配置,以便可以使用Github的众多强大的功能。

2.3 package.json

package.json文件,这是发包时唯一不可或缺的文件。一个最精简的package.json文件是执行npm init生成的这个版本:

{
  "name": "npm-speci-test", // 组件名
  "version": "1.0.0",       // 组件当前版本
  "description": "",        // 组件的一句话描述
  "main": "index.js",       // 组件的入口文件
  "scripts": {              // 工程化脚本,使用npm run xx来执行
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",             // 组件的作者
  "license": "ISC"          // 组件的协议
}

作为一个规范的组件,我们还需要考虑: 1. 我的代码托管在什么位置了 2. 别人可以在仓库里通过哪些关键词找到组件 3. 组件的运行依赖有哪些 4. 组件的开发依赖有哪些 5. 如果是命令行工具,入口文件是哪个 6. 组件支持哪些node版本、操作系统等

一份比较通用的package.json文件内容如下:

{
  "name": "@scope/xxxx",
  "version": "0.1.0",
  "description": "description:xxx",
  "keywords": "keyword1, keyword2,...",
  "main": "./dist/index.js",
  "bin": {},
  "scripts": {
    "lint": "eslint --ext ./src/",
    "test": "npm run lint & istanbul cover _mocha -- test/ --no-timeouts",
    "build": "npm run lint & npm run test & gulp"
  },
  "repository": {
    "type": "git",
    "url": "http://github.com/xxx.git"
  },
  "author": {
      "name": "someone",
      "email": "someone@gmail.com",
      "url": "http://someone.com"
  },
  "license": "MIT",
  "dependencies": {},
  "devDependencies": {
    "eslint": "^5.2.0",
    "eslint-plugin-babel": "^5.1.0",
    "gulp": "^3.9.1",
    "gulp-rimraf": "^0.2.0",
    "istanbul": "^0.4.5",
    "mocha": "^5.2.0"
  },
  "engines": {
    "node": ">=8.0"
  }
}
  • name属性要考虑的是组件是否为public还是private,如果是public要先确认该名称是否已经被占用,如果没有占用为了稳妥起见,可以先发一个空白的版本;如果是private的,则需要加上@scope前缀,同样也需要确认名称是否已被占用。
  • version属性必须要符合semver规范,简单理解就是:
    • 第一个版本一般建议用0.1.0
    • 如果当前版本有破坏性变更,无法向前兼容,则考虑升第一位
    • 如果有新特性、新接口,但可以向前兼容,则考虑升第二位
    • 如果只是bug修复,文档修改等不影响兼容性的变更,则考虑升第三位
  • keywords会影响在仓库中进行检索的结果
  • main入口文件的位置最好可以固定下来,如果组件需要构建,建议统一设置为./dist/index.js, 如果不需要构建,可以指定为根目录下的index.js
  • scriptsscripts通常会包含两部分:通用脚本和自定义脚本。无论是个人还是团队,都应该为通用脚本建立规范,避免过于随意的命名scripts;自定义脚本则可以灵活定制,比如:
    • 通用scripts:start、lint、test、build
    • 自定义scripts:copy、clean、doc等
  • repository属性无论在私有环境还是公共环境,都应该加上,以便通过组件可以定位到源码仓库
  • author 如果是一个人负责的组件,用author,多个人就用contributors

2.4 流程

保障并行多个版本,并且每一个发布的版本可回溯即可,统一采用分支开发,用master作为线上分支和预发分支,开发分支要发版需要预先合并到master上,然后再master上review和单测后直接发布,并打tag标签

2.5 README.md && changelog

 

Readme文件是对项目的基本介绍,直接关系到组件能不能更容易被他人使用。组件的readme文件一般包括以下部分内容:

  • 组件基本介绍
  • 组件开发环境
  • 组件使用说明(重要)
  • 组件API参考(重要)
  • 注意事项

changelog一般是记录每个版本更新的差异,可以单独用一个文件记录,也可以在readme底部记录,功能或者api的变更一定要有相应的log

2.6 Demo

对一个组件而言,demo的重要性不言而喻,demo更侧重于具体场景中的用法,一般在项目根目录的example文件夹存放项目的demo,对于较小的组件,也可以和readme合并

2.7 单元测试

单元测试需要一定的开发成本,对于业务类的组件,可以不写单元测试。

但是对于功能组件、UI组件就很有必要写单元测试,以保障组件的可用性和质量。

 

基于vue cli3的多产品差异化打包优化

前言

背景

同一份代码包含多个产品实现,核心逻辑相同,只是ui、接口、少量逻辑有差异,通过不同打包命令打包对应产品的代码

旧的实现

通过对应打包命令,注入相应环境变量,获取变量通过js逻辑展示对应资源或逻辑。这种方式虽然能够实现对应产品的展示和逻辑,但是存在多余逻辑和资源。

优化

优化思路

在编译阶段注入环境变量,通过模板语法输出对应产品逻辑和资源,打包之后的代码就很纯净。

优化方案

自定义loader,使用ejs引擎批处理js文件

vue.config.js

chainWebpack: (config) => {

 config.resolveLoader.alias.set('my-loader', path.resolve(__dirname, 'myLoader.js'))
 config.module.rule('js')
 .use('my-loader')
 .loader('my-loader')
 .options({
 ENV : process.env.VUE_APP_EVN,
 PRODUCT : process.env.VUE_APP_PRODUCT
 })
 .end()
 }

my-loader.js

'use strict'
const loaderUtils = require('loader-utils');
const ejs = require('ejs');

module.exports = function (source) {
 var options = loaderUtils.getOptions(this) || {}
 return ejs.render(source, options)
}

问题

vue cli3默认添加了cache-loader来提升打包的效率,这也造成在切换产品打包时并不会并不会触发重新打包

解决

  1. 简单粗暴,每次编译之前删除 node_modules\.cache\babel-loader 目录 , /doge
    cross-env rimraf node_module/.cache/babel-loader && vue-cli-service build
  2. js文件不使用 cache-loader
config.module.rule('js').uses.delete('cache-loader')

这两种方式虽然可以解决问题,但是缺点明显,有点捡了芝麻丢了西瓜的感觉。而且,如果在不切换产品的时候使用cache提升打包速度,显然上诉的做法是无法实现的。最求完美的我们继续探索第3种方法:

  • 通过cacheKey判断是否命中缓存文件
config.module.rule('js')
 .use('cache-loader')
 .loader('cache-loader')
 .options({
    cacheIdentifier : `${process.env.VUE_APP_PRODUCT}:${process.env.VUE_APP_EVN}`
    // cacheKey : (options, request) => {
      // return `${process.env.VUE_APP_PRODUCT}:${process.env.VUE_APP_EVN}:${options}`;
    // }
 })

一个另类思路的前端脚手架

市面上的前端脚手架几乎都是命令行式问答方式交互操作,然后拉取模板,这种方式不仅体验极差展示有限,而且当选项很多的时候很容易出错。因为大家使用pc进行工作,如果有一个可视化的程序来构建项目,那么体验和扩展能力就大大增强了,所以我就产生了一个开发能在桌面运行脚手架程序的念头。顺便学习一下使用Electron开发跨平台桌面客户端和Vue3的新特性。

整体开发思路

1.使用vue3开发UI界面,以及实现交互操作

2.打包成Electron客户端(windows端)

3.打包成exe安装包(看需要进行加壳处理)

开发步骤

项目搭建

网上有个比较火的脚手架electron-vue,但electron-vue是vue-cli2.0的版本,现在都已经出 4.0 了,再者electron-vue已经很久没有更新,我们可以使用 vue 最新的脚手架插件vue-cli-plugin-electron-builder来搭建项目。

用vue-cli创建一个vue3项目

在项目内集成Electron

进入我们项目的根目录,我们执行以下命令来安装插件vue-cli-plugin-electron-builder

vue add vue-cli-plugin-electron-builder

选择最新11.0.0版本

依赖包有些大,需要等待一段时间

项目实现

思路:

主进程监听渲染进程发送的事件和参数:

主进程 ipcMain.on(“send”, (event, arg) => { console.log(arg); });

渲染进程 ipcRenderer.send(“send”,’来自渲染进程’);

根据不同的事件和参数,拉取对应远程模板,并使用ejs预处理,调整好目录,删除多余文件,完成初始化。

未完待续。。。

项目打包

electron-builder打包

electron-builder有比electron-packager有更丰富的的功能,支持更多的平台,同时也支持了自动更新。除了这几点之外,由electron-builder打出的包更为轻量,并且可以打包出不暴露源码的setup安装程序。

默认已经集成打包方式,但是依赖包下载失败率很高😒,后面再展开研究

 

electron-packager打包

npm install electron-packager --save-dev

package.json添加script

"build:exe":"electron-packager . HelloWorld --platform=win32 --arch=x64 --icon=public/favicon.ico --out=./exe --asar --app-version=0.0.1 --overwrite --ignore=node_modules",

运行 npm run build 打包vue项目生成dist项目文件

运行 npm run build:exe 生成exe文件夹和HelloWorld-win32-x64

拷贝dist整个文件夹到HelloWorld-win32-x64目录下的resources目录中,并修改文件夹名称为app

此时运行HelloWorld.exe,第一个桌面端应用程序就完成了。

但是这种方式我们还需要手动打包成exe安装文件

NSIS打包electron程序为exe安装包

打开NISI:

1)选择可视化脚本编辑器(HW VNISEdit)

2)选择新建脚本:向导

3)设置应用名称,版本号,网址,标志随便自定义一个就好,然后下一步

4)设置安装程序图标(图标必须是ico格式),名称,语言(SimpChinese),界面,然后下一步

5)授权文件有就填,没有就填空白   然后下一步

6)默认两个文件选中,删除 , 添加应用程序文件

7)添加我们之前打包生成的exe文件, 选择HelloWorld-win32-x64文件夹,选中包含子目录,确定,然后下一步

8)可修改开始菜单名称,然后下一步

9)可设置安装成功后启动的程序,默认就是我们打包后的启动程序,下一步

10)设置一些卸载时界面的提示信息,然后下一步

11)保存我们的脚本,完成,保存到桌面

12)打开脚本文件,开始编译,需要一段时间

13)编译运行完成之后我们就得到了一个exe安装包

我们打包的exe安装包,在安装的时候有可能会被某些“安全”软件当做病毒处理,这时候需要把安装包进行加壳处理,涉及到另外一个领域,我们有空在研究。

不过话说回来,我们打包脚手架程序是给内部使用,如果遇到这种问题可以通过添加信任等方式处理。

rollup打包按需加载的组件

rollup介绍

rollup 是一个 JavaScript 模块打包器,可以将小块代码编译成大块复杂的代码,例如 library 或应用程序。Rollup 对代码模块使用新的标准化格式,这些标准都包含在 JavaScript 的 ES6 版本中,而不是像CommonJS 和 AMD这种特殊解决方案。( 也有插件可以处理成umd模式 )

rollup最大的亮点就是Tree-shaking,即可以静态分析代码中的 import,并排除任何未使用的代码。这允许我们架构于现有工具和模块之上,而不会增加额外的依赖或使项目的大小膨胀。如果用webpack做,虽然可以实现tree-shaking,但是需要自己配置并且打包出来的代码非常臃肿,所以对于库文件和UI组件,rollup更加适合。

一,安装依赖

npm i rollup –save-dev

二,配置文件

rollup 的配置文件是在 rollup.config.js 中配置的,由于rollup 本身会处理 这个配置文件,所以我们可以使用 ESmodules 语法来写

我们执行 rollup –config 就可以生成打包后的文件了,我们只有带了 –config ,rollup才会执行我们的配置文件,默认是不会执行我们的配置文件的

external属性

使用rollup打包,我们在自己的库中需要使用第三方库,例如lodash等,又不想在最终生成的打包文件中出现jquery。这个时候我们就需要使用external属性。比如我们使用了lodash

 

三,rollup插件

  •  rollup-plugin-terser 用于压缩js代码
  • @rollup/plugin-node-resolve让 rollup 能够识别node_modules的第三方模块。
  • @rollup/plugin-commonjs将 CommonJS 的模块转换为 ES2015 供 rollup 处理。

利用babel来编译es6代码

首先我们先安装babel相关模块:

npm i core-js @babel/core @babel/preset-env @babel/plugin-transform-runtime
设置.babelrc文件

module.exports = {
presets: [[“env”, { “modules”: false }]],
plugins:[
[“@babel/plugin-syntax-dynamic-import”],
[“external-helpers”]
]
}

@babel/preset-env可以根据配置的目标浏览器或者运行环境来自动将ES2015+的代码转换为es5。需要注意的是,我们设置”modules”: false,否则 Babel 会在 Rollup 有机会做处理之前,将我们的模块转成 CommonJS,导致 Rollup 的一些处理失败。

为了解决多个地方使用相同代码导致打包重复的问题,我们需要在.babelrc的plugins里配置@babel/plugin-transform-runtime,同时我们需要修改rollup的配置文件:

babel({
  exclude: 'node_modules/**',    
  runtimeHelpers: true   // 使plugin-tramsform-runtime生效
})  

 

rollup 的优势和缺点

 

优点:

  • 输出结果更加扁平,执行效率更高,
  • 会自动移除未引用的代码(内置了 tree shaking)
  • 打包结果的可读性比较好

 

缺点:

  • 加载第三方模块比较复杂
  • 模块被打包到一个函数中,不能实现 HMR (html-modules-replace)模块热替换
  • 浏览器环境中,代码拆分功能必须依赖 amd 库,

 

打包器选择:

当我们使用第三方插件比较多时,我们选用webpack,当我们写一些自定义插件,库,或者框架的时候(vue、react…),我们使用rollup 作为模块打包器,webpack 大而全,rollup 小而美

 

注意事项

  • node版本最好是10.0.0以上
  • 插件的引用顺序可能会引起报错

 

遇到的坑

1.Babel 7.0.0-beta.56 has dropped support for the ‘helpersNamespace’ utility.

解决方案:

安装babel 6.x

npm install –save-dev rollup-plugin-babel@3

npm install –save-dev babel-core

npm install –save-dev babel-upgrade

 

总体思路:

  • 编写多个组件,暴露各自独立入口
  • 编写整体入口文件
  • 编写打包脚本
    1.     编写打包入口文件列表或者自动获取
    2.     遍历入口分别使用rollup.rolup方法进行打包
    3.     通过内置generate方法获取打包后的数据,fs.writeFileSync创建对应文件
  • 添加自定义打包script(注意运行打包脚本前需要先删除出口文件夹)
  • 配置package.json相应字段
  • 运行命令,打包发布

 

遍历

// 打包模式列表
const formatTypeList = [
  { format: 'umd', min: false, suffix: '.js' },
  // { format: 'cjs', min: true, suffix: '.common.min.js' },
  // { format: 'umd', min: false, suffix: '.umd.js' },
  // { format: 'umd', min: true, suffix: '.umd.min.js' }
  // { format: 'es', min: false, suffix: '.js' }
  // { format: 'es', min: true, suffix: '.es.min.js' }
]
// 创建目录
fs.mkdirSync('lib')
fs.mkdirSync(getAssetsPath('style'))
let packages = []
formatTypeList.forEach(({ format, min, suffix } = {}) => {
  entries.forEach(({ input, output , module }) => {
    packages.push({ min, format,suffix,module, input, output: `${output}`})
  })
})
build(packages)

打包

const { output, suffix, input, format, module , min } = config
  const inputOptions = {
    input,
    external: Object.keys(external),
    plugins : plugins
    // plugins: min ? plugins.push(terser()) : plugins
  }
  const fullName = output + suffix
  const file = getAssetsPath(fullName)
  const outOptions = {
    file,
    format,
    name: module,
    globals: external
  }
  console.log(rollup);
  const bundle = await new rollup.rollup(inputOptions)
  let { output: outputData } = await bundle.generate(outOptions)
  await write({ output: outputData, fileName: output, format, fullName, file })

创建文件

for (const { type, code, source } of output) {
    if (type === 'asset') {
      const cssFileName = `${fileName}.css`
      const filePath = getAssetsPath(`/style/${cssFileName}`)
      // rollup.write(filePath,source.toString())
      !fsExistsSync(filePath) && fs.writeFileSync(filePath,source.toString())
    else {
      const filePath = file
      let codeSource = code.replace(/\s?const\s/g, ' var ')
      fs.writeFileSync(filePath, codeSource)
    }
  }

 

vue项目sentry配置

前言

默认已经拥有 sentry 后台

系统地址:  xxx

以及对应的账号的前提下,首先在后台的账号管理中添加对应的权限:

  1. api – Auth Token – create new Token,
  2. 勾选 project:releases 和 project:write
  3. 然后点击 create Token
  4. 复制生成后的 Auth Token

接着新建一个项目,选择 Vue 模板,填写项目名称,注意项目名称不能带有 _ 下划线。

然后在项目设置中在到 Client Keys(DSN) 的 一栏, 记录一下 DSN 值, 下面会用到。

 

需要安装的一些依赖包

另外,要进行详细配置需要查看详细的文档 sentry

下面讲到的步骤都是基于webpack4

安装配置依赖

首先安装上述提到的依赖包,在项目根目录执行以下命令

npm i -D @sentry/webpack-plugin raven-js

编写 .sentryclirc

这份东西一定要有,并且只能是这个名字,并且只能在你项目的根目录,编写的格式如下:

  • token 在 sentry 后台账号设置里面能找到,而且必须是已经设置了文章开头的权限
  • url sentry后台链接
  • org 账号对应的组织
  • project 项目名称
[auth]
token=your_token
 [defaults]
url = your_sentryUrl
org = your_org
project = your_project_name

假如我不想起这个名字,并且不想放在根目录,那就要起另外一个文件 sentry.properties,这个文件的格式如下:

defaults.url=
defaults.org=
defaults.project=

auth.token=

@sentry/webpack-plugin

这个插件的作用是负责将编译打包出来的sourcemap 上传到 sentry 后台,所以要检查一下编辑打包中是否都把 sourcemap 开启

基本配置下面这些就够用了

new SentryCliPlugin({
    include: './dist',
    // ignoreFile: '.sentrycliignore',
    ignore: ['node_modules'],
    // 版本
    release:  '',
    deleteAfterCompile: true,
    // 指定配置文件的位置
    configFile: '',
    urlPrefix: `~/`,
})

几个特别需要注意的配置:

  • release 这个是版本号,这个可以用于将错误区分项目的版本和环境,比如 project-1.0.0-testing 将错误归纳到项目1.0.0版本测试环境
  • configFile 注意,@sentry/webpack-plugin 的 configFile 如果要指定配置文件,一定只能是 sentry.properties 这种格式的文件,不然会出错
  • urlPrefix 为了精确定位源码位置,假如 项目其中一个js链接 http://www.a.com/b/1.js, 那么 ~ 代表 http://www.a.com,所以这是的 urlPrefix 就应该是 ~/b,这个东西一定要设置好,不然后台看到的错误代码只会是压缩成一行的,设置对的话才会源码显示

详细配置信息

  • release [optional] -版本的唯一名称,必须为string,应唯一标识您的版本,默认为sentry-cli releases propose-versioncommand,该命令应始终返回正确的版本
  • include [required] – string或array,Sentry CLI应该递归扫描源的一个或多个路径。它将上传所有.map文件并匹配关联.js文件
  • ignoreFile [optional] – string,指向包含要忽略的文件/目录列表的文件的路径。可以指向.gitignore或具有相同格式的任何内容
  • ignore [optional]– string或array,在上传过程中要忽略的一个或多个路径。覆盖ignoreFile文件中的条目。如果既没有ignoreFile或ignore存在时,默认为[‘node_modules’]
  • configFile [optional] – string,Sentry CLI配置属性的路径,如 https://docs.sentry.io/learn/cli/configuration/#properties-files中所述。默认情况下,从当前路径向上查找配置文件,~/.sentryclirc并且始终从加载默认文件
  • ext [optional] – string,添加要考虑的其他文件扩展名。默认情况下,将处理以下文件扩展名:js,map,jsbundle和bundle。
  • urlPrefix [optional] – string,这会在所有文件前面设置一个URL前缀。默认为,~/ 但是您可能希望将其设置为完整URL。如果您的文件存储在子文件夹中,这也很有用。例如:url-prefix ‘~/static/js’
  • validate [optional] – boolean,如果未启用重写功能,则会在上传之前尝试进行源地图验证。它将发现源地图的各种问题,如果发现任何问题,将取消上载。这不是默认设置,因为这可能导致误报。
  • stripPrefix [optional] – array,与rewrite此配对将从上传的文件中截取前缀。例如,您可以使用它来删除特定于构建计算机的路径。
  • stripCommonPrefix [optional] – boolean,与rewrite此配对将添加~到stripPrefix数组中。
  • sourceMapReference [optional] – boolean,这会阻止自动检测源映射引用。
  • rewrite [optional] – boolean,允许重写匹配的源映射,以便尽可能平整索引的映射并内联丢失的源。默认为true
  • dryRun [optional] – boolean,尝试空运行(适用于开发环境)

Raven-js 注册

在你的全局Vue实例化之前,对 Raven-js进行注册:

import Vue from 'vue'
import Raven from 'raven-js'
import RavenVue from 'raven-js/plugins/vue'

// 这里的DSN就是在后台项目设置中拿到的DSN
Raven.config([DSN], {
        // 这里的release要和@sentry/webpack-plugin配置的release一样
        release: ``,
        // 环境
        environment: ‘’,
        // 这里对一些错误进行了级别的定义
        dataCallback(data) {
            if(/Only secure origins are allowed/.test(data.message)) data.level = 'warning'
            return data
        },
        // 白名单 以下链接下的报错才上报
        // 通常这里填写项目的线上链接,防止被刷
        whitelistUrls: [
            ``
        ],
        // 忽略一些错误不上报
        ignoreErrors: [
            /SecurityError\: DOM Exception 18$/
        ]
}).addPlugin(RavenVue, Vue).install()

另外自己写的模块也可能出现出错的情况,这是还需要使用 window.onerror 监听一下

window.onerror = (msg, url, line, col, error) => {
            //没有URL不上报!上报也不知道错误
    if ((msg == "Script error." && !url) || !error) {
        return
    }
    this.log({
        error,
        type: 'script handle'
    })
}

对静态访问资源失败做监听:

window.addEventListener(
    'error',
    (event):boolean => {
        console.log(event)
        // 过滤 js error
        const target = event.target || event.srcElement;
        const isElementTarget =
            target instanceof HTMLScriptElement ||
            target instanceof HTMLLinkElement ||
            target instanceof HTMLImageElement;
        if (!isElementTarget) {

            return false;
        }
        // 上报资源地址
        const url =
            target.src ||
            target.href;

        this.log({
            error: new Error(`ResourceLoadError: ${url}`),
            type: 'resource load'
        });
        return true
    },
    true
);

上报信息优化

合并类似的错误

相同的报错,例如 Cannot find variable 'id',在不同浏览器显示的错误信息不一样,但其实都是相同的报错,这时可以通过正则对这些错误进行合并;

对无用的上报信息进行过滤

  1. 有时候错误信息只有undefined,或者是空的,这时可以通过SDK的过滤器进行过滤
  2. 对白名单域名的js出现的错误才进行上报
  3. 在后台设置合适的过滤条件,如 浏览器限制

提供更清晰的上报信息

  1. 提供 设备执行环境 、用户信息 等上下文,
  2. 为接口请求报错提供 error - requestUrl的标题,并提供整个请求的上下文信息

为错误设置合适的等级

错误的等级意味着影响项目正常运行的严重程度,需要更优先地进行处理

  • fatal 已经影响到整个项目的运行
  • error 只影响到单独功能的运行
  • warning 有错误,但不影响项目功能运行

前端异常监控与报警

我们的需求点?

  • 有些灵异问题只存在于线上特定的环境
  • 前端没有一个紧急问题报警的系统

异常分类

  • javascript异常
    • 语法错误
    • 运行时错误
    • script文件内错误(跨域和未跨域)
  • JS文件、CSS文件、img图片等(资源)的404错误(其实是有onerror事件的dom)
  • promise的异常捕获
  • ajax请求错误

异常级别

一般而言,我们会将收集信息的级别分为info,warn,error等,并在此基础上进行扩展。

当我们监控到异常发生时,可以将该异常划分到“重要——紧急”模型中分为A、B、C、D四个等级。有些异常,虽然发生了,但是并不影响用户的正常使用,用户其实并没有感知到,虽然理论上应该修复,但是实际上相对于其他异常而言,可以放在后面进行处理。

一般而言,越靠近右上角的异常会越快通知,保证相关人员能最快接收到信息,并进行处理。A级异常需要快速响应,甚至需要相关负责人知悉。

错误捕获

  1. 主动捕获(try catch / promise catch)
  2. 全局捕获(onerror / addEventListener)

try catch

通常,为了判断一段代码中是否存在异常,我们会这一写:

try {
var a = 1;
var b = a + c;
} catch (e) {
// 捕获处理
console.log(e); // ReferenceError: c is not defined
}
使用try catch能够很好的捕获异常并对应进行相应处理,不至于让页面挂掉,但是其存在一些弊端,比如需要在捕获异常的代码上进行包裹,会导致页面臃肿不堪,不适用于整个项目的异常捕获。

 

onerror事件

/** *

@param {String} msg 错误信息 *

@param {String} url 出错文件 *

@param {Number} row 行号 *

@param {Number} col 列号 *

@param {Object} error 错误详细信息 */

window.onerror = function (msg, url, row, col, error) {

console.log({ msg, url, row, col, error })

return true; // 注意,在返回 true 的时候,异常才不会继续向上抛出error;

};

打印如下:  {

msg: “Uncaught ReferenceError: error is not defined”,

url: “file:///Users/Desktop/test.html”,

row: 25,

col: 5,

error: ReferenceError: error is not defined at

}

 

通常我们使用window.onerror来捕获js脚本的错误信息。但是对于跨域调用的js脚本,onerror事件只会给出很少的报错信息:error: Script error.这个简单的信息很明显不足以看出脚本的具体错误,
通过为页面上的 script 标签添加 crossOrigin 属性完成跨域上报,别忘了服务器也设置 Access-Control-Allow-Origin 的响应头。(解决跨域的js脚本错误上报)
onerror事件 是无法捕获到网络异常的错误(资源加载失败,裸奔,图片显示异常等)。当我们遇到 <img src="./404.png"> 报 404 网络请求异常的时候,onerror 是无法帮助我们捕获到异常的。

 

window.addEventListener监听error事件

由于网络请求异常不会事件冒泡,因此必须在捕获阶段将其捕捉到才行,但是这种方式虽然可以捕捉到网络请求的异常,但是无法判断 HTTP 的状态是 404 还是其他比如 500 等等,所以还需要配合服务端日志才进行排查分析才可以。

<script>

/** * @param {String} event 监听事件 *

@param {function} function 出错文件 *

@param {Boolean}

useCapture 指定事件是否在捕获或冒泡阶段执行。 * true – 事件句柄在捕获阶段执行 * false- false- 默认。

事件句柄在冒泡阶段执行 */

window.addEventListener(‘error’, (error) => {

console.log(‘我知道 404 错误了’);

console.log( error ); return true; }, true);

</script>

 

<img src=”./404.png” alt=””>

 

promise异常捕获

现代的浏览器其实已经能够支持promise语法了,所以在promise异常捕获这一块我们也还是要注意一下.

  • 人工手动catch捕获(这个是基本的,和try…catch…是一样的).
  • 通过浏览器自带的unhandledrejection事件来监听全局没有catch的promise执行.但是这个的兼容性不是很好,具体可以看下unhandledrejection
<script>
  window.addEventListener('unhandledrejection', function(err) {
    console.log(err);
  });
</script>

new Promise(function(resolve, reject) {
  reject(new Error('haha'))
})

打印如下:

bubbles:false
cancelBubble:false
cancelable:true
composed:false
currentTarget:Window
defaultPrevented:false
eventPhase:0
isTrusted:true
path:Array(1)
promise:Promise // 捕获到的错误promise
reason:Error: haha at http://localhost:3000/promise_error:21:12 at Promise (<anonymous>) at http://localhost:3000/promise_error:20:3 // 其实就是错误栈
  message: "haha"
  stack: "Error: haha↵ at http://localhost:3000/promise_error:21:12↵ at Promise (<anonymous>)↵ at http://localhost:3000/promise_error:20:3"
returnValue:true
srcElement:Window
target:Window
timeStamp:55.190000000000005
type:"unhandledrejection"

 

异常如何上报

监控拿到报错信息之后,接下来就需要将捕捉到的错误信息发送到信息收集平台上,常用的发送形式主要有两种:

  • 通过 Ajax 发送数据(xhr和jquery)
  • 动态创建 img 标签的形式
    function report(error) {
      var reportUrl = 'http://xxxx/report';
      new Image().src = reportUrl + 'error=' + error;
    }
xhr的形式的一个弊端是url长度限制,上报的数据量要做好控制

sourceMap

我们目前的js文件都有做代码压缩、混淆,fis和webpack都比较常用uglifyJs,压缩之后的代码会导致我们很难定位到真实的报错位置
 解决方案是开启构建工具代码压缩的source-map功能:

例如:webpack中开启source-map功能:

module.exports = {

devtool: ‘#source-map’,

}
打包压缩的文件末尾会带上这样的注释:

!function(e){var o={};function n(r){if(o[r])return o[r].exports;var t=o[r]={i:r,l:!1,exports:{}}…;
//# sourceMappingURL=bundle.js.map
意思是该文件对应的map文件为bundle.js.map。下面便是一个source-map文件的内容,是一个JSON对象:

version: 3, // Source map的版本
sources: [“webpack:///webpack/bootstrap”, …], // 转换前的文件
names: [“installedModules”, “__webpack_require__”, …], // 转换前的所有变量名和属性名
mappings: “aACA,IAAAA,KAGA,SAAAC…”, // 记录位置信息的字符串
file: “bundle.js”, // 转换后的文件名
sourcesContent: [“// The module cache var installedModules = {};…”], // 源代码
sourceRoot: “” // 转换前的文件所在的目录

 

因为前端解析速度较慢,所以这里不做推荐,我们还是使用服务器解析。如果你的应用有node中间层,那么你完全可以将异常信息提交到中间层,然后解析map文件后将数据传递给后台服务器;

通过mozilla/source-map的SourceMapConsumer接口,可以通过将转换后的行号列号传入Consumer得到原始错误位置信息。相应的node代码如下

var fs = require(‘fs’)

var sourceMap = require(‘source-map’) // map文件

var rawSourceMapJsonData = fs.readFileSync(‘./dist/index.min.js.map’, ‘utf-8’)

rawSourceMapJsonData = JSON.parse(rawSourceMapJsonData)

var consumer = new sourceMap.SourceMapConsumer(rawSourceMapJsonData); // 打印出真实错误位置

console.log(consumer.originalPositionFor({line: 1, column: 220}))

 

通过上面方式得到错误行号。

 

MVVM框架

在MVVM框架中如果无法使用window.onerror来捕获异常,因为你的异常信息被框架自身的异常机制捕获了。比如Vue 2.x中我们应该这样捕获全局异常:

Vue.config.errorHandler = function (err, vm, info) {
let {
message, // 异常信息
name, // 异常名称
script, // 异常脚本url
line, // 异常行号
column, // 异常列号
stack // 异常堆栈信息
} = err;

// vm为抛出异常的 Vue 实例
// info为 Vue 特定的错误信息,比如错误所在的生命周期钩子
}
目前script、line、column这3个信息打印出来是undefined,不过这些信息在stack中都可以找到,可以通过正则匹配去进行获取,然后进行上报。

 

注意点

 

以上是异常捕获和上报的主要知识点和流程,还有一些需要注意的地方,比如你的应用访问量很大,那么一个小异常都可能会把你的服务器搞挂,所以上报的时候可以进行信息过滤和采样等,设置一个调控开关,服务器也可以对相似的异常进行过滤,在一个时间段内不进行多次存储。

 

后话

目前也有一些非常完善的前端监控系统存在,如sentrybugsnag等,针对目前我们的情况,拿来开源的第三方应该更方便一下,但是了解原理,摸清逻辑还是很有必要滴~~~~

记前端私有NPM仓库的搭建

相关准备

  • 作为服务器记的机器一台。
  • nodejs。
  • pm2(node 进程守护)。
  • sinopia(私有npm仓库服务)。

首先安装nodejs,具体就不多说了。。。

安装 sinopia

# 安装
npm install -g sinopia

# 验证是否安装成功 --> 这一步会输出自动生成的配置文件路径等信息。
sinopia

注册一个默认账户

为了提高安全性,我们稍后会禁用 sinopia 的用户注册功能,所以先注册一个默认的 sinopia 账户。需要在当前 shell 中执行 sinopia 命令开启服务之后,再重新打开一个 shell 执行:

npm set registry http://localhost:4873/
npm adduser --registry http://localhost:4873/

配置 sinopia

配置文件路径可以在执行 sinopia 命令时,从其输出中查看,一般应是 /home/pi/.config/sinopia/config.yaml

基于我的使用使用经验和文档说明,主要配置了以下内容:

  • max_users: -1 :禁用注册。
  • npmjs: url: https://registry.npm.taobao.org : 设置 npm 镜像为淘宝源,一来可以加速 npm 公共包的安装,二来借助淘宝源的只读特性,避免误操作发布私有 npm 包到外网上。
  • access: $authenticated:禁止匿名用户访问。配置后,未登录用户看不到 sinopia 上私有包的任何信息。
  • max_body_size: ‘200mb’:这样设置,会提高安装超级 npm 包的成功率,比如 react-native 。

htpasswd 配置

config.yaml 中的 max_users: -1 表示我们将最大用户数设置为-1,表示禁用 npm adduser 命令来创建用户,不过仍然可以通过目录下的 htpasswd 文件来初始化用户, 打开 htpasswd 文件很明显密码被加密了,可使用小插件 htpasswd-for-sinopia, 添加用户。

$ npm install htpasswd-for-sinopia -g // 安装

$ sinopia-adduser // 在sinopia目录下执行,按照提示输入用户名密码

 

 

pm2:进程守护管理工具

$ npm install -g pm2

$ pm2 start `which sinopia`

 

 

深坑!深坑!深坑!

通过sinopia的 github , https://github.com/rlidwka/sinopia

不难发现,已经很久没有更新了,这里遇到了一个比较大的坑~~~~
使用nrm切换到私有npm仓库对应的源后,下载带@ 符号的包都下载失败,比如下载 @angular/core,就会下载失败,这是为什么呢,查阅了一些资料,发现这其实是Sinopia自己的bug,bug产生的原因就是:sinopia在代理到npmjs.org公有库时将@符号转码为%40,致使在公有库中找不到对应的包,返回404 ,简单点说就是 @angular/core 代理请求的时候被转换成了 %40angular/core,所以我们需要在代理请求发出之前将其转回 @angular/core
如何解决?
修改sinopia源码:修改位于sinopia/lib/up-storage.js文件第10行:将var encode = encodeURIComponen;,更改为:var encode = function(thing) {return encodeURIComponent(thing).replace(/^%40/, ‘@’);}; ,这段代码的含义就是将%40转回@,于是就解决了不能下载带有@符号的npm包的bug
更好的解决方案?
由于sinopia的作者已于二年前停止对sinopia的维护和升级,所以出来了一个sinopia的fork,名字叫做Verdaccio,然后由Verdaccio继续对sinopia进行更新和维护,具体如何使用Verdaccio来构建私有npm服务器,请见Verdaccio的github

https://github.com/verdaccio/verdaccio

 

所以还是推荐使用  verdaccio!

 

记前端私有NPM仓库的使用

写在前面:

为了更好的开发和维护公共组件;

为了将目前的项目打包的主要阵地慢慢从fis3转移到webpack的怀抱;

为了更规范的开发流程;

这就是NPM私有仓库的初衷。

 

使用指南

为了更好的兼容和管理旧的项目,强烈建议先安装 NVM  (node版本控制器),因为一些旧的项目在没有更好的解决方案之前还需要使用 FIS进行构建和发布 ,例如:fis3+smarty的项目,但是fis3已经停止更新,一些插件已经不兼容新版本的node!!!

 

安装nvm之前,如果已经安装nodejs,建议先卸载; 安装步骤:(windows系统)

一,安装nvm和nodejs

1,下载安装包(下载地址: https://github.com/coreybutler/nvm-windows/releases),安装;

2,打开命令行:

列出全部可以安装的版本号

 

nvm ls-remote

 

安装指定版本

 

 

nvm install v6.9.5  #命令后加版本号就可以进行安装,字母v可以不写

建议安装6.9.5版本兼容fis3的项目(本人亲测)

再安装 8.10.0 以上的版本,处理webpack打包,以及私有仓库的管理

 

用切换指定版本,切换效果是全局的

nvm use v8.11.1

至此nvm和node已经安装完成

 

二,安装nrm(npm镜像源管理工具

使用nvm切换node到高版本

 

nvm use v8.11.1

安装

npm install -g nrm

 

添加私有仓库地址

nrm add flamingo http://dqdnpm01.gz.xxx.com:4873/

切换私有仓库

nrm use flamingo

 

查看所有仓库地址(星标为当前仓库源)

nrm ls
npm ---- https://registry.npmjs.org/
cnpm --- http://r.cnpmjs.org/
taobao - https://registry.npm.taobao.org/
nj ----- https://registry.nodejitsu.com/
rednpm - http://registry.mirror.cqupt.edu.cn/
npmMirror https://skimdb.npmjs.com/registry/
edunpm - http://registry.enpmjs.org/
*flamingo http://dqdnpm01.gz.xxx.com:4873/

 

至此,已经可以在本地使用npm install  所需要的公共组件了,注意:公共组件包名规则     @flamingo/包名   ,如果私有仓库找不到该包,会到npm官方镜像查找,包的上传请移步到    发布指南

 

发布指南

发布首先需要账号,已经禁用   npm adduser   的方式添加用户,需要账号请联系xx!

 

发布方式和npm包的发布一样,

首先,

npm login
然后,进入到要发布的包的目录之后

npm publish

 

发布命令很简单,这里重点讲一下发布的规范。

 

注意事项

1,发布的公共组件最好不要直接使用ES6及以上的语法,因为一般不会使用babel对node_modules的模块进行编译。或者可以使用babel编译之后在发布,并注明依赖;

2,模块化方案推荐使用commonjs规范;

3,package name 一定要使用 @flamingo/包名   的方式( @flamingo 相当于是一个命名空间,暂时使用这个) ,  所以引入的时候有也要用   require(“@flamingo/包名”)

发布规范

需要发布的包必须包含, js脚本,package.json,README.md

js脚本不用多少,就是主要功能代码,可以有多个文件,需要指定入口,入口配置在 package.json 文件中;

package.json

  • name:@flamingo/包名
  • version:你这个包的版本,默认是1.0.0
  • description:这个用一句话描述你的包是干嘛用的
  • entry point:入口文件,默认是index.js,你也可以自己填写你自己的文件名
  • test command:测试命令,目前还不需要这个。
  • git repository:这个是git仓库地址,可不需要。
  • keyword:包的关键字。
  • author:你的账号
  • license:可不需要

 

README.md
这是一个mackdown文件(mackdown语法请自行谷歌),是你该组件的使用文档,一般包含以下内容:(如果是发布到官方,README.md文件一般是英文书写,我们这里只最为内部私有公共模块,所以可以使用中文书写或者再添加一个README_zh.md的中文使用文档)
  1. 你的项目介绍
  2. 你的代码实现了什么功能?
  3. 该如何使用? (系统环境参数,部署要素)
  4. 代码组织架构是什么样的?
  5. 版本更新重要摘要

关于XX移动端官网第一次重构

前言

接手这个项目已经半年有余,半年的需求迭代也使这个原本的小型项目慢慢变大,逻辑也慢慢更复杂;也是因为之前的整体架构设计扩展性、复用性太差,导致现在维护起来越来越蛋疼;各种活动加版本迭代(产品真是玩出了花)已经需要3个分支同时进行开发;代码的冗余也随之而来,总之,我终于受不了。。。

说干就干:

分析阶段

1,原先的整体架构是FIS3构建+JQ,考虑到有些页面是要内嵌到APP,如果做SPA,客户端也要配合更新,代价太大,而且经过和产品的沟通,我也没有那么多的时间来重构(只能默默抽时间来搞),最后还是选择FIS3+SASS+zepto+modJS+ES6+artTemplate 的技术架构。

2,项目中最乱的就是各种自定义弹窗(大概几十个),风格不一,代码也五花八门,代码也想当耦合;纯粹的业务逻辑堆叠式开发,会导致功能无法重用;

3,统计埋点的解耦,之前的埋点统计逻辑和业务逻辑混合在一起;

4,公共模块的抽出与打包策略

设计阶段

1,和设计妹纸商量统一页面风格,弹窗风格

2,设定目录规范,配置相应的构建配置,搭好整体架子,模块化采用conmmonjs规范

3,构建utils全局公共方法类,拆分模块,细化模块

4,使用promise重新封装全站ajax请求,封装公用异步事件

5,引用弹窗组件,并在此基础上二次封装全站自定义弹窗成一个模块

6,在body上做事件委托,点击的时候进行递归查询父节点是否含有该私有属性,进行相应处理。解耦统计埋点

7,分离移动端和PC端项目

实现阶段

目录结构:

app: 页面程序入口

components: 全站的模块化组件和依赖需要模块化加载的库( 没有做区分 )

lib:需要提前加载的库

src: 全站引用的资源( 图片 | 脚本(包括公共脚本) | 样式 )

tpl:模板存放目录( 不需要发布 )

部分主要配置:

// 引入模块化开发插件,设置为 commonjs 规范。
fis.hook('commonjs',{
 baseUrl: '/components',
 extList: ['.js', '.es']
});
// 启用node-sass 插件 , 解析 .scss 后缀为
fis.match('*.scss', {
 rExt: '.css',
 parser: fis.plugin('node-sass', {}),
});
// 启用 es6-babel 插件,解析 .es6 后缀为 .js
fis.match('**.es', {
 rExt: '.js',
 // parser: fis.plugin('es6-babel'),
 parser: fis.plugin('babel-5.x'),
 isMod: true
});
/*************************主目录规范*****************************/
// ------ 配置src 目录
fis.match("/src/**", {
 isMod: true,
 release: '${project.static}/$&'
});
fis.match("/lib/**", {
 isMod: false
});
fis.match("/components/{**,/**}", {
 isMod: true,
 useSameNameRequire: true,
 release: '${project.static}/$&'
});
fis.match("/components/{**.html,/**.html}", {
 release: false
});
// ------ 配置app 目录
fis.match("/app/**", {
 isMod: true,
 release: '${project.static}/$&'
});
/************************* 文件处理 *****************************/
// ------ 配置css压缩 md5 启用精灵图
fis.match('**.{css,scss}', {
 useHash: true,
 useSprite: true,
 preprocessor : fis.plugin("autoprefixer",{
 "browsers": ["Android >= 2.1", "iOS >= 4", "ie >= 8", "firefox >= 15"],
 "cascade": true
 }),
 optimizer: fis.plugin("clean-css")
});
// ------ 配置html压缩
fis.match('*.html', {
 optimizer: fis.plugin('html-compress')
});
// ------ 配置图片md5
fis.match('{**.jpg,**.png,**.gif}', {
 useHash: true,
});
// ------ 配置图片压缩
fis.match('*.png', {
 optimizer: fis.plugin('png-compressor')
});
// ------ 配置js压缩
fis.match('**.{js,es}', {
 useHash: true,
 optimizer: fis.plugin("uglify-js", {
 mangle: {
 except: 'exports, module, require, define',
 eval: true
 },
 compress: {
 drop_console: true,
 keep_fargs: false
 }
 })
});
/*************************文件处理 END *****************************/
// ------ 配置打包
fis.match('::package', {
 // npm install [-g] fis3-postpackager-loader
 // 分析 __RESOURCE_MAP__ 结构,来解决资源加载问题
 postpackager: fis.plugin('loader', {
 resourceType: 'commonjs',
 useInlineMap: true // 资源映射表内嵌
 })
});


/************************* 打包策略 *****************************/
fis.match("components/{*,**/*}.{js,es}", {
 packTo: "pkg/components.js",
});
fis.match("app/{*,**/*}.{js,es}", {
 packTo: "pkg/app.js",
});
fis.match("src/js/{*,**/*}.{js,es}", {
 packTo: "pkg/base.js",
});
fis.match("components/{*,**/*}.{css,scss}", {
 packTo: "pkg/components.css",
});

1,利用前端模板引擎和FIS3的资源引入能力,分离模板和页面;把数据和渲染完全分开;

2,函数式编程思想,使流程更加清晰

3,公共脚本注册全局时间,注册全局模板过滤器

4,  统一页面初始化流程,程序结构更加清晰

例如:统一处理页面链接上的参数

 replaceHref(param){
 const _keys = Util.getUrlKey();
 // 检查链接如果没有带渠道号 ,如果本地有 把本地的渠道号加到链接上
 // 因为检查UDID的时候已经把链接上的渠道号存入本地,
 // 所以只需检查本地有无渠道号把本地渠道号加到链接上即可
 const _localchannelid = this.getCookie("xxipa_storage_channelid");
 if (_localchannelid) {
 _keys.channelid = _localchannelid;
 }
 this.dataType(param)=='Array' && JSON.stringify(_keys) !== "{}" && param.forEach(function(v,i){
 _keys.hasOwnProperty(v) && delete _keys[v];
 })
 // 根据传过来的key重新处理链接上的参数
 history.replaceState && history.replaceState(null, "", window.location.href.split('?')[0] + (this.param(_keys) == "" ? "":"?"+this.param(_keys)) );
 }

5,公共流程的抽出,以及回调的设计,本次都是采用promise的方案,但是感觉也并不是那么理想,下次准备使用 es7的 async 和 await 重新设计

 

最后,关于项目重构的理解

 

首先,前端项目重构的基础是必须要先理解透彻业务逻辑,做到能够把控全局,然后分析代码,设定优化方案;评估重构的影响面及可能的风险;

然后,业务流程拆分,功能模块拆分,各个击破;

最后,当然是要全面的测试了。

 

 

目前重构版本已经上线一个月了,再修复了几个隐藏逻辑的bug之后,目前也比较稳定,最近几次活动增删逻辑,也可以游刃有余的应对,不过也感觉还有一些可以优化的空间。

总之,只要项目还需要迭代,项目就还需要重构!谁也不知道产品下一个需求又会有什么样的脑洞。。。

关于的新版H5游戏中心的总结

项目开始前的思考:

  • 采用动静结合的方式,缓解服务器压力(然而也没什么压力)
  • 组件化、模块化,采用CMD规范(fis3-smarty不是那么兼容amd,好吧我太懒没有深入研究这块,最后用了百度提供的mod.js,也挺好用)
  • 数据和结构分离(smarty、art-template)

项目依赖的工具与技术:

  • fis3
  • fis3-smarty
  • mod.js
  • art-template
  • 插件:es6-babel、node-sass、autoprefixer 等。

项目的目录结构:

components目录:项目所用到的js组件(js库、功能模块等)

page目录:页面模板目录

plugin目录:本地模拟数据预览用到的模板解析插件(可忽略)

static目录:存放图片,公共样式,公共脚本

test目录:本地模拟数据

widget目录:模板组件目录


fis-conf.js主要配置
fis.hook('commonjs',{
baseUrl: 'components/',
extList: ['.js', '.es']
});
fis.match("components/**", {
isMod: true,
useSameNameRequire: true
});
fis.match("widget/**", {
isMod: false,
useSameNameRequire: true
});
/ 启用 es6-babel 插件,解析 .es 后缀为 .js
fis.match('**.es', {
  rExt: '.js',
  parser: fis.plugin('es6-babel')
});
// 启用node-sass 插件 , 解析 .scss 后缀为 .css
fis.match('*.scss', {
  rExt: '.css',
  preprocessor : fis.plugin("autoprefixer",{
    "browsers": ["Android >= 2.1", "iOS >= 4", "ie >= 8", "firefox >= 15"],
    "cascade": true
  }),
  parser: fis.plugin('node-sass', {
    // include_paths: [
    //   './src/scss',
    //   './components/compass-mixins'
    // ]
  })
});
// widget目录下存放的html文件是arttemplate模板文件,不需要发布
fis.match('widget/{*,**/*}.html', {
    release: false
});

需要动态加载的数据的组件使用前端模板的优点是:

1.提高可维护性 2.代码结构更加清晰 3.数据更容易控制 4.降低耦合性

(此处为什么要用html作为模板后缀名,主要是利用fis对html 文件编译处理会重新定位资源路径,目前尝试了一些配置去改变模板文件的后缀,但是还没有找到可行的方法; 模板文件会在编译阶段直接引入到js文件,依赖fis在js文件的内容嵌入功能:__inline(‘xxx.html’))

 

 

存在的问题:

最主要的问题是缓存,由于页面都是需要后端生成,静态的页面会有缓存