DDFE/DDFE-blog

滴滴 webapp 5.0 Vue 2.0 重构经验分享

ustbhuangyi opened this issue · 56 comments

项目背景

滴滴的 webapp 是运行在微信、支付宝、手 Q 以及其它第三方渠道的打车软件。借着产品层面的功能和视觉升级,我们用 Vue 2.0 对它进行了一次技术重构。

技术栈

MVVM框架: Vue 2.0
源码:es6
代码风格检查:eslint
构建工具:webpack
前端路由:vue-router
状态管理:vuex
服务端通讯:vue-resource

技术全景

技术全景图

几个问题

  1. 滴滴 webapp 是一个大的 SPA 应用么?
    滴滴 webapp 包含众多业务线,每个业务线都有独立的一套的发单流程逻辑。那么问题来了,这些业务逻辑都是在一个单页中完成的么?

  2. 如何实现组件化?
    滴滴 webapp 5.0 的设计思路就是组件化,设计提供了很多组件,每个页面都是由组件拼接而成。那么问题来了,如何区分基础组件和业务组件,并把基础组件抽象成一个公共组件库?

  3. 一个代码仓库多条业务线,如何很好的做到多人同时开发和持续集成?
    滴滴有多条业务线,每条业务线会有一位前端同学开发代码。那么问题来了,如何模块化的组织代码,如何尽可能的减少开发的冲突以及做好持续集成?

  4. 有部分业务线需要异步加载,这部分业务线如何开发?
    滴滴目前会把类专车业务线的代码放在一个仓库里,但是部分业务线,如顺风车的代码是不在这个仓库里的。那么问题来了,这部分代码如何开发,如何使用 Vue,Vuex,store,以及一些公用方法和组件?

  5. 异步加载的业务线组件,如何动态注册?
    我们需要异步加载业务线的 JS 代码,这些业务线实现的是一个 Vue component。那么问题来了,如何优雅地动态注册这些组件呢?

  6. 异步加载的业务线如何动态注册路由?
    我们在使用 Vue-router 初始化路由的时候,通常都会写一个路由映射表。那么问题来了,这些异步加载的业务线,如果也想使用路由,那么如何动态注册呢?

  7. 如何在测试环境下与后端接口交互?
    我们在开发阶段,通常都是在本地调试,本地起的服务域名通常是 localhost:端口号。那么问题来了,这样会和 ajax 请求产生跨域问题,而我们也不能要求服务端把所有 ajax 请求接口都开跨域,如何解决呢?

  8. 如何部署到线下测试环境?
    我们在本地开发完代码后,需要把代码提测。通常测试拿到代码后,需要部署和测试,那么问题来了,我们如何把本地代码部署到我们的开发机测试环境中呢?

解决方案

  1. 滴滴 webapp 是一个大的 SPA 应用么?

    滴滴 webapp 包含众多业务线,每个业务线都有独立的一套的发单 -> 接驾 -> 行程中 -> 订单完成的流程逻辑。试想一下,如果整体是一个 SPA 应用,最终打包的 JS 会变的很大,虽然可以通过 code spliting 技术异步加载,但也不可避免会增加代码量,而且非常不利于维护。

    因此,我们把发单和后续的业务逻辑拆开,拆成发单首页和后续流程页面,每个业务线都有自己独立的发单后的流程页面。这样滴滴的 webapp 相当于多个 SPA 应用构成,页面间跳转的数据传递通过 url 传参即可。

  2. 如何实现组件化?

    组件化现在几乎成为 webapp 开发的标准,滴滴从设计角度就已经是组件化的思路了。但是设计只会把页面拆成一个个组件,我们作为开发者,需要从这些众多组件中提取出哪些是基础组件,哪些是业务组件,哪些组件可被复用等等。

    基础组件主要指那些本身不包含任何业务逻辑、可以被轻松复用的组件,例如 picker、timepicker、toast、dialog、actionsheet 等等...我们基于 Vue 2.0 实现了一套移动端的基础组件库,打包了所有基础组件,并托管在 npm 私服上,使用非常方便。基础组件的通讯基本就是往组件传入 prop,并监听组件 $emit 的事件。

    业务组件主要指那些包含业务逻辑,包括一些与后端接口通讯的逻辑。业务组件会包含若干个基础组件,通常我们会把一些业务逻辑的数据通过 Vuex 管理起来,然后组件内部读取数据和提交对数据修改的动作。这里需要说明一点,当我们使用 Vuex 的时候,并不是所有和后端通讯的请求都需要提交一个 action,如果这个请求并不会修改我们 store 里的数据,可以在组件内部消化。举个实际的例子,我们在开发 suggest 组件的时候,每次输入字符检索相关的地址的时候,这个请求由组件内部发起,并且把请求的数据渲染到组件的列表即可,因为它并没有修改 store 里的数据。

    基础组件通常都是可复用的,部分业务组件同样可复用,它们的 UI 和业务逻辑相似。我们会把单个可复用的业务组件单独发布到 npm 私服上,需要使用的业务线依赖即可。注意,业务组件我们是不建议使用 Vuex,需要考虑到不同的使用方对 Vuex 内部变量的定义和使用是不相同的。

  3. 一个代码仓库多条业务线,如何很好的做到多人同时开发和持续集成?

    滴滴的 webapp 首页有多条业务线,每条业务线都有一个开发人员,为了保证尽量减少代码的冲突,我们按业务线对代码进行了模块划分。由于 Vuex 支持modules,我们很自然地按业务线拆分了 modules,每个 modules 维护自己的 getters、actions、mutaions 和 state,而一些公共数据如经纬度、上下车信息、用户登录状态等作为 root state,被所有业务线共享。同样,components 里也按业务线做了更细致的划分,每个业务线独立的业务组件放在各自的目录里,彼此之前不会有冲突。

    仅仅做到目录拆分还是不够的,我们还要考虑到持续集成,跟着产品的版本迭代节奏发布上线。那么每个版本的需求,每个业务线都会参与开发,我们用 gitlab 管理代码,如果每个开发同学都拉一个分支,那么会面临着分支太多,功能联调麻烦等问题。因此,我们约定了一套 git 的管理规范,每个大需求版本,我们会约定以 "dev +上线时间日期" 作为分支名创建开发分支,所有人在这个分支上开发,开发完成让 QA 测试该分支,上线前才会将分支合入主干发布。在两个版本发布期间如果有 bug fix,则约定以 "bugfix + 功能描述" 为分支名创建 bugfix 分支,修复完成后合入主干上线。每次上线前,我们都会运行脚本新增版本号,编译打包,保证前端资源的增量发布。

  4. 有部分业务线需要异步加载,这部分业务线如何开发?

    滴滴目前会把一些业务线的代码放在一个仓库里,但是部分业务线,如顺风车的代码是不在这个仓库里的。首页通过异步加载 JS 去加载这部分业务线的代码,这部分业务线很显然也是需要用 Vue 开发的,但是他们不可以再去单独引入 Vue.js。

    我们的解决方案是在 window 上注册一个 XXApp 对象,把 Vue、Vuex 以及一些公共组件和方法等挂载到这个对象上,那么这些异步加载的业务线就可以通过 window.XXApp 访问到了,代码如下:

    window.XXApp = {
        Vue, 
        Vuex,
        store, // 全局store
        saveCurrentBiz, // 公共方法
        Location // 公共组件
        // 其它一些公共方法和组件
    }
    

    业务线可以访问到这些对象后,接下来需要实现的就是一个 Vue component。

  5. 异步加载的业务线组件,如何动态注册?

    Vue.js 官网提供的异步组件的解决方案大多是基于 webpack 的 require.ensure 去异步加载组件的,但很显然这并不适用滴滴的业务场景,因为我们的代码并不在一个仓库下。我们需要一种类似 amd 的解决方案,这些异步业务线需要实现的是一个 Vue component,我们该如何优雅地动态注册这个 component 呢?

    我们知道 Vue 提供了动态注册组件的 api,通过 Vue.component('async-example',function(resolve){ //... }) 的方式在工厂函数里通过 resolve 方法动态注册组件。注意,这个工厂函数的执行时机是组件实际需要渲染时,那我们渲染这些异步组件的时机就是当我们切换顶部导航栏到该业务线的时候

    首先,每一条业务线对应着一个独立的组件,业务线有各自的 id,因此,我们先用一个对象去维护这样的映射关系,代码如下:

    const modules = {
        业务线id: Taxi, // 出租车
        // 其它同步业务线组件  
    }
    

    这个对象初始化的都是同步业务线组件,对于异步加载的业务线组件,我们需要动态注册。首先我们在全局的 config.js 里维护一个业务线的配置关系表,异步加载的业务线会多一个 src 属性,代码如下:

    bizConf: {
       异步业务线id: {
          name: 'alift', // 业务线名称
          src: xxx // 加载异步业务线的 js 地址
       },
       同步业务线 id: {
          name: 'taxi'
       }
       // 其它业务线配置
    

    接下来我们遍历这个对象,代码如下:

     // 获取 bizConf 对象
     const bizJSConf = config.get('bizConf') 
    
     for (let id in bizJSConf) {
        let conf = bizJSConf[id]
        if (conf.src) {
          modules[id] = conf.name
          Vue.component(conf.name, (resolve, reject) => {
            loadScript(conf.src).then(() => {
              resolve(modules[id])
            }).catch(() => {
              reject()
            })
          })
        }
      }
    

    可以看到,对于异步业务线,我们会把它的 name 添加到 modules 对象的映射关系中,并按这个 name 注册一个异步组件,注意,这个时候注册组件的工厂函数并不会执行。

    我们之前说到了渲染这些异步组件的时机就是当我们切换顶部导航栏到该业务线的时候,我们来看看切换顶部导航栏的时候执行了什么逻辑,关键代码如下:

     this.currentView = modules[productid]
    

    这个 currentView 我们是在 App.vue 的 data 里初始化的,映射到 template 的代码如下:

     <component :is="currentView"></component>
    

    没错,这里我们又用到一个 Vue 的高级用法,动态组件。我们的业务线组件对应的就是这个动态组件。官网文档介绍的动态组件是绑定到一个组件对象上的,这对于我们的同步组件,当然是没有问题的,modules 映射的就是一个组件对象;但是对于异步组件,我们映射的是组件的名称,它是一个字符串,当 currentView 指向这个字符串的时候,注册异步组件的工厂函数执行了,回顾之前的代码,这个时候它会去加载异步业务线的 js,加载完成的回调函数里,执行 resolve(modules[id])

    等等,看到这里,有人不禁会问,这里 modules[id] 是什么,还是异步组件的名称吗?当然不是了,这里的 modules[id] 对应的是异步业务线的组件对象。那么,它是怎么被赋值成组件对象的呢?我们来看代码:

    window.XXApp = {
        // ...
        // 一些公共方法和组件
        registerBiz(id, component) {
          modules[id] = component
        }
    }
    

    我们在 window.XXApp 下又添加了一个 registerBiz 的方法,当我们异步加载完业务线的 JS 后,异步业务线调用这个方法真正的把自己实现的 Vue component 注册到我们的 modules 里,所以我们 resolve 的就是这个组件对象,是不是很优雅?至此,我们完成了异步业务线组件的动态注册。

  6. 异步加载的业务线如何动态注册路由?
    再接着上述问题继续发散,我们在使用 Vue-router 初始化路由的时候,通常都会写一个路由映射表。对于同步业务线这些已知的组件,路由的映射是没有问题的,那么这些异步加载的业务线,如果它的某些子组件也想使用路由该怎么办?

    我们需要一套动态注册路由的方案,而官网文档提供的路由懒加载的方案并不能满足我们的需求,因此我们想到了另一种变通方案。我们在路由配置如下:

      {
        path: 'pathA' //这里的命名只是示意
        component: componentA
      },
      {
        path: 'pathB',
        component: componentB
      },
      //...
      {
        path: '/:name',  // 动态匹配
        component: Dynamic // 已知组件
      }
    

    可以看到,我们在定义了一系列常规的路由后,最后定义了一个动态匹配路由,也就是任意 name 的一级 path,只要没有命中之前的 path,都会映射到这个我们定义好的 Dynamic 组件上。我们来看看这个 Dynamic 组件的实现,先看一下模板:

    <template>
      <transition :name="transitionName">
        <component :is="currentRouter"></component>
      </transition>
    </template>
    

    本质上,Dynamic 组件还是利用了 Vue 的动态组件,通过修改 currentRouter 这个变量,可以改变当前渲染的组件。我们来看一下这个 currentRouter 修改的逻辑:

    created() {
      this.setCurrent()
    },
    methods: {
      setCurrent() {
        const name = this.$route.params.name
        const component = this.routes[name]
        if (component) {
          this.currentRouter = component
        }
      }
    }
    

    在组件创建的钩子函数里,我们会调用 this.setCurrent() ,该方法首先通过路由参数拿到 name,然后从 this.routes[name] 拿到对应的组件,并赋给 this.currentRouter 。那么 this.routes 变的尤为重要了。我们实际上是把 routes 存储到了 Vuex 的 store 里, 然后通过 Vuex 的 mapGetters 获取的:

     computed: {
      ...mapGetters([
        'routes'
      ])
    },
    

    既然通过 Vuex 的方法可以获取 this.routes ,我们一定会有写的逻辑,而这个存的逻辑实际上就是我们提供给这些异步业务线提供了一个 api 接口实现的:

    window.XXApp = {
        // ...
        // 一些公共方法和组件
    	registerRouter(name, component) {
          Vue.component(name, component)
          store.commit('ADD_ROUTES', {
            name,
            component
          })
        }
    }
    

    我们提供了 registerRouter 接口,参数就是路由的名称和对应的组件实例,我们首先通过 Vue.component 全局注册这个组件,然后通过 Vuex 提供的 commit 接口提交了一个 ADD_ROUTES 的 mutation,来看一下这个 mutation 的定义:

     [types.ADD_ROUTES](state, data) {
       state.routes = Object.assign({}, state.routes, {
         [data.name]: data.component
       })
     },
    

    至此,我们就完成了 routes 的存取逻辑,整个动态路由方案也就完成了, 异步业务线想使用动态路由,只需要调用我们提供的 registerRouter 接口,是不是很方便呢~

  7. 如何在测试环境下与后端接口交互?

    我们在开发阶段,通常都是在本地调试,本地起的服务域名通常是 localhost:端口号。这样会产生一些接口的跨域问题,除了常规的一些跨域方案,我们实际上可以借助 node.js 服务帮我们代理这些接口。

    我们借助 vue-cli 脚手架帮我们生成一些初始化代码。在 config/index.js 文件中,我们修改 dev 下 proxyTable 的配置,代码如下:

     proxyTable: {
      '/xxxservice': {
        target: 'http://xxx.com.cn', //你的目标域名
        changeOrigin: true
      },
      //...
    }
    

    实际上,它就是利用了 node.js 帮我们做了一层服务的转发,这样就可以解决开发阶段的跨域问题了。

  8. 如何部署到线下测试环境?

    我们在本地开发完代码后,需要把代码提测。通常测试拿到代码后,需要部署和测试,为此我们写了一个 deploy 的脚本。原理其实很简单,就是利用一个 scp2 的 node 包上传代码,它的执行时机是在 webpack 编译完成后,代码如下:

    var client = require('scp2')
    //...
    webpack(webpackConfig, function (err, stats) {
        // ...
    	client.scp('deploy/home.html', {
    	    host,
    	    username,
    	    password,
    	    path
    	  }, function (err) {
    	    if (err) {
    	      console.log(err)
    	    } else {
    	      console.log('Finished, the page url is xxx')
    	    }
    	  })
     })
    

总结

技术的重构总伴随着产品的升级,从这次大重构中,我们对 Vue 有了更深入的理解和掌握。对于它的周边插件如 Vuex 和 Vue-router,我们团队的小伙伴也有了较深入的研究,产出几篇文章也在这里和大家分享:
Vuex 2.0 源码分析
vue-router源码分析-整体流程
vue-router 源码分析-history

以上,欢迎拍砖~

好文。之前使用相同技术栈重构了项目。有些功能模块刚用vue2实现,在重构中,就采用了原先后端路由跳转的方式。不足之处是需要多一次加载vue.js。
“异步注册路由”和“异步加载注册组件”没有尝试。涨知识了

托管在 npm 私服是如何进行维护和版本管理呐?动态加载的组件是如何处理编译大包的问题?@ustbhuangyi

@Thinking80s ,npm私服维护和版本管理和公共的 npm 没什么区别,只不过只能滴滴内部发布和下载罢了。动态加载的组件仍然用 vue-cli 初始化项目,编译打包都是一样的,只需要调用我们的接口把组件注册即可。

@ustbhuangyi我们现在遇到了业务模块和公共组件一起webapck打包后引用图片路径问题,公共组件和业务模块的图片资源放在各自路径下(公共组件和业务模块是git库分开管理)。

@Thinking80s , 图片通过 webpack 打包后要么会 base64 到 js 代码中(小图片),要么就会打包到对应的目录(一般是 img/),然后跟着其它静态资源(js、css)一起打包上线就可以了。注意要配置 publicPath,确保线上访问的路径正确。所以公共组件和业务模块分别编译打包上线就行了。

请问动态注册路由这里,如何处理参数问题?

情况是这样的:
1.公共组件库是单独项目,各个组件引用各自样式及背景图片css中background-url('./img')
2.业务组件模块也是单独项目,业务模块中使用公共组件,业务模块也是引用各自项目的样式

遇到的问题:公共组件库+业务组件模块一起用webapck打包后,导致引用图片路径不正确,现在做法是把公共组件库的img图片文件夹同时拷贝一份业务组件模块来使用。
@ustbhuangyi 请问下你们采用哪种方式处理这个问题?

@fengluo ,我们这个动态注册的路由不支持传参数,所有数据的通信我们都通过 Vuex

@Thinking80s ,你们应该是分开用 webpack 打包吧,公共组件你需要配置 publicPath,然后编译打包后,引用图片的地址就变成的绝对路径,然后你需要把这些图片上传到线上,保证引用地址的正确。不过我们目前组件库还没有超过 10k 大小的图片,大部分图标都是 icon-font,所以图片都 base64 到 js 里了

@ustbhuangyi 公共组件与业务组件是同级目录,公共组件没有单独打包是合在一起打包的,webapck通过alias别名引用全局路径方式,公共组件没有采用全局注册的方式引用。

@Thinking80s 建议分仓库维护、分别打包。另:我们的组件库是通过 Vue.component 全局注册的

npm私服是用Sinopia吗

@Rococolate 用的 Artifactory

感谢分享

code split我觉得用webpack.ensure也可以的,用lerna管理整个项目,比靠约定感觉还好些。

动态注册路由真的好么,为什么不开始的时候就引入。动态又难debug,万一要ssr时又会蛋疼……统一管理也一目了然,即使要放在模块文件夹下,觉得也是先写好好……

即使动态注册vuex modules,也要确保namespace不冲突,一直觉得vuex没有把elm architecture似的隔离做的特别好。

测试环境部署,可以ci来build,ci来构建docker image,ci上传registry,测试环境pull镜像……比如某司……

其实我最想问……一年前的项目感觉升级不到vue2了怎么办,没有测试也没有qa不敢重构怎么办……

@reverland
滴滴这个场景是不适合用 webpack.ensure 的,因为是动态加载其它业务线的代码,压根代码就不在一个仓库下,只能通过 loadscript 方式加载,所以也有动态注册路由的需求。
技术重构往往伴随着产品重构,单纯的技术重构不太现实,除非特别闲。。所以慢慢来吧,新项目可以用 vue2 了~

啊,仓库已经分开了确实。只是觉得也可以设计成放一起的。

也是,产品重构再说吧……眼看新项目半年就变成旧项目……

@ustbhuangyi那么通过loadscript 方式加载的业务代码是怎么打包的?也是通过webpack单独项目打包方式吗?

请问,代码是 如何划分目录结构为好? 有推荐的划分目录方式吗?
比如这样:

---app.js
    |
    ---actions
    |    |
    |    ---XX.Action.js
    |    ---YY.Action.js
    ---componets 
    |    |
    |    ---User.js
    |    ---Home.js
    ......

@Thinking80s 对,业务线的 JS 是单独通过 webpack 打包的

请问,针对安卓4.2以下手机是如何处理的?

@callmedadaxin 这么低版本的,基本忽略了吧

@CYTGithub 基本不低于 4.4 吧

@CYTGithub 需要配置.babelrc 文件,配置 plugins: ['transform-runtime']

@ustbhuangyi 业务线的 JS 是单独通过 webpack 打包情况下,那么相同的公用库或组件应该会存在重复的存在了。如:vue + vue-resource这样的npm包了。

@Thinking80s , 看问题 4

你好,我们也用vue2.0重构的,但是我们在项目中遇到了一个bug,就是在微信端 iphone6s 10.2.1 系统 ,用户快速滑动页面的时候,页面还没有停止下来,就点击页面上的按钮,就会造成页面卡死(这个bug必现)。这个时候点击左上角返回无效,只能点击关闭退出才可以。然后我们把fastClick.js取消,就可以了,但是会有300ms的延迟,不知道贵司有遇到过这种问题吗?谢谢

上一版的 webApp 是用百度的 fis 在服务端构建的是吧,想了解一下 fis 和 webpack 用哪个好?在项目实践中各有什么优劣?谢谢。

@liujinjian1016 ,我手机恰好就是 10.2.1 的 iphone6s ,并没有复现你的问题~ 而且我们也用了 fastclick 库,也是没问题的。你也可以看看微信里的滴滴出行,通过你们的操作能复现问题么?

@bison1994 ,上一版确实是基于 scrat(fis的二次开发)构建的。由于我们用的 Vue,Vue 脚手架官方推荐就是 webpack,对应的 vue-loader 插件也是很强大。fis 和 webpack 比,确实社区,活跃度都要差很多,我曾经给 scrat 提过要支持 vue 2.0 预编译,1 个月也没有答复,所以。。果断换了 webpack。其实相对于 webpack,我对 fis 更加了解,毕竟百度大部分用的都是 fis,我在百度的时候也在 fis 基础上二次开发过,而且我也阅读过 fis 的源码,甚至用 gulp 插件模拟过 fis 大部分功能。fis 的设计理念也很不错(插件化、资源依赖关系表等),说的有点啰嗦了,总结几点吧:

  1. webpack 对于 Vue 项目而言,支持程度非常好,可以看到 vue-cli 也在不断完善。webpack 和 react 的项目好像也是天然配。
  2. webpack 的社区真的很不错,插件丰富,贡献者多,遇到问题大部分都可以找到解决方案。
  3. webpack 对于写一些独立的 JS 库也非常友好,并且 webpack 2 的 tree-shaking 技术也是弥补了和 rollup 的差距。
  4. fis 在百度内部使用很广泛,如果了解它,可以基于它做一些二次开发,fis 比较适合一些传统项目。

确实很多新东西出来之后 fis 的 parser 和 processor 总是比 webpack 的 loader 慢半拍

@ustbhuangyi 目前fis3的vue 2.0预编译已经支持了 fis3-parser-vue-component

并推荐一下我使用fis3 + typescript + vue 做的一个vue官方的 vue-hackernews-2.0 的 demo
https://github.com/Treri/fis3-typescript-vue-hackernews-2.0

@treri 恩,这个我知道,不过我们已经全部迁移到 webpack 了~

@ustbhuangyi 不知道你们使用webpack的时候有没有遇到两个问题

1 moduleId 基于数字不稳定的问题. 这个问题我在另一个issue中和别人讨论过 chemdemo/chemdemo.github.io#10 (comment)

webpack 的 moduleId 对于永久缓存的支持.

假设我们有这样一个 SPA 页面场景, 有一个路由文件 route.js, 会在此文件中使用 webpack 的 code spliting 写法, 按需加载文件.

在 route.js 中分别对应三个页面(foo, bar, baz), 按需加载对应的入口文件. 在入口文件中, 会再分别加载各个子页面依赖的文件.

  • foo.js
    • dep1.js
    • dep2.js
  • bar.js
    • dep2.js
    • dep3.js
  • baz.js
    • dep1.js
    • dep3.js

当我们使用 webpack 进行 build 后, webpack 会给每个文件分配一个 moduleId, 这个 moduleId 是随着 webpack 处理到的文件的数目而进行递增的. 一种结果是类似于下面这样的, 文件后面的括号中是生成的 moduleId 号

  • foo.js (1)
    • dep1.js (4)
    • dep2.js (5)
  • bar.js (2)
    • dep2.js (5)
    • dep3.js (6)
  • baz.js (3)
    • dep1.js (4)
    • dep3.js (6)

如果我们需要在foo.js中增加一个依赖 dep4.js, 那么相应的 moduleId 会变成类似于下面这样

  • foo.js (1)
    • dep1.js (4)
    • dep2.js (5)
    • dep4.js (6)
  • bar.js (2)
    • dep2.js (5)
    • dep3.js (7)
  • baz.js (3)
    • dep1.js (4)
    • dep3.js (7)

我们只增加(或删除)了一个文件, 却导致了多个文件的 moduleId 变化, 这样就导致多个文件的内容发生了变化. 那么当重新发布后, 其实有的页面的内容根本不需要变化, 但是仅仅是因为 moduleId 变化, 而导致需要重新下载这些文件, 使得没法使用浏览器已经缓存的文件.

如果你的页面有几十个, 每次添加或者删除一个文件后, 都会导致几乎所有的文件的浏览器缓存失效.

虽然现在已经有了namedModulePlugin 插件, 但是总觉得不是很好用

2 CommonChunksPlugin 的 minChunks 提取公共模块的问题
在route中使用code split时, 我想把公共模块提取出来, 所以我使用commonChunksPlugin.

如果我把minChunks 设置为最小值, 即把所有的公共模块全都提取出来, 但是这个公共文件就会很大. 其实某个view可能只依赖了其中一个模块, 却要把整个文件下载下来 , 冗余性太高.

如果我把minChunks 设置的大一些, 就会出现, 有的公共模块被两个view使用, 但是不满足 minChunks 的要求, 所以没有被提取到 common.js 中, 导致这个模块在所有依赖它的View中分别打包了一次. 同样也造成了代码的冗余.

@treri ,你说的 webpack 的问题确实存在,这个是和 webpack 的实现相关,如果你的 code splitting 做的很细的话确实会很蛋疼。目前我们这边主要还是基于 vue-cli 创建的一套 webpack 配置,用的commonChunksPlugin 把 node_modules 部分的代码打包成一个 vendor.js,所以还没有你说的那个问题。至于 code splitting,就是我文章描述的那样的异步加载方式,不过这个是比较有滴滴特色的方式,一般的搞法还是基于 webpack 的 reuqire.ensure,就可能出现你说的那个问题。

@treri

zhenyong/Blog#1
对于你的问题 1 ,文中也许有你需要的相关信息

@zhenyong 谢谢. 周末两天, 写了一下原来使用 webpack 的时候遇到的一些问题, 还有切换到 fis 之后对项目的一些调整. 如果有兴趣可以看一下

使用 FIS3 构建 Vue 前端工程. webpack 用户也请看过来

@treri 看你极力安利 fis3,我泼点冷水吧~ fis3 有 405 个 open 的 issue,也好久没人维护了,社区贡献的 fis 插件基本也都是 fex 的人搞的吧~ 这样的构建工具,如果是放在大公司,估计也就百度比较适合用了(毕竟是他们搞的)。

@ustbhuangyi 谢谢~

只是我没觉得我在安利 fis3 😁, 只是就像文章最上面说的, 我用 webpack 的时候确实遇到了一些问题解决不了. 但是用 fis3 解决却比较自然, 所以用的就比较多而已.

webpack 大牌, 紧跟社区, 当然也想用, 方便和大家交流. 但是就像你说的, webpack实现本身有问题, 而我又想把 code split做的细一些, 我不能容忍冗余加载, 使用 webpack 的时候又不能完全避免冗余加载.

fis3 issue 比较多, 维护力度也不够. 这个确实比较可惜. 但是就我自己目前使用来看, 还没有遇到什么问题, 可能是我的用法都是比较大众的用法吧 😁

如果您有什么办法能解决我文章中的问题的话, 还请不吝告知, 因为我知道使用 fis3 是小众, 但是使用webpack时遇到的问题我也是真的很纠结

Vue.component(conf.name, (resolve, reject) => {
        loadScript(conf.src).then(() => {
          resolve(modules[id])
        }).catch(() => {
          reject()
        })
      })

想问下

  1. 这里在线上动态注册组件时, conf.src 这个文件应该是组件单独编译打包的成一个文件的吧,所以这里你们是怎么把单独组件编译打包成一个文件的呢?是根据组件配置表遍历后逐个编译?

  2. 本地开发环境 conf.src 这个引用的也是组件编译后的js文件还是类似 .vue 的源码文件?

@ustbhuangyi想问下

线上动态注册组件时, 异步加载的组件单独编译打包的成一个文件的吧?,怎么把单独组件编译打包成一个文件的呢?

很棒!

业务组件我们是不建议使用 Vuex

不是基础组件吗?

@bigggge

你引用的句子后面还有一句

需要考虑到不同的使用方对 Vuex 内部变量的定义和使用是不相同的

当你的业务组件在不同业务场景,或者不同平台使用,或许可以称呼该业务组件为「基础组件」

如果硬要咬字,那么我们应该称呼与 Vuex 相关的「组件」为「业务容器」,不同的平台或者业务场景下,可能有不同的数据来源,也许会有不同的「业务容器」,但是也许会复用同一个基础的"业务组件"

666,感谢分享干货,已推荐到 SegmentFault 头条 (๑•̀ㅂ•́)و✧
链接如下:https://segmentfault.com/p/1210000011520846

@ustbhuangyi

Vue.component(conf.name, (resolve, reject) => {
        loadScript(conf.src).then(() => {
          resolve(modules[id])
        }).catch(() => {
          reject()
        })
})

问题:
(1)loadScript 函数的实现 是用的npm库,还是如何实现的,猜想你里面应该是promise的写法去动态创建scsript标签,插入异步业务线的 js 地址(单独基于webpack打包的),
(2)业务线代码仓库下的 JS开发 是直接编写vue格式文件的全局组件定义方式吗?
(3)这个组件的开发,他所依赖的npm包 在那里安装了?

建议给个例子!

thezj commented

mark

异步业务线如果用vue-router 并且异步业务线有子路由和动态路由怎么处理呢? 其实你这里的动态路由只是用了动态组件去切换异步组件,如果异步组件自己有vue-router路由,你这个就没办法了

@krisyangjian 其实当时做的时候动态添加路由的 API vue-router 是没有提供的,所以我们自己实现了一套简单的,比较粗糙,但是异步组件也是可以动态添加路由的。现在新版的 vue-router 已经对外提供了这个 API,直接用就好了

@ustbhuangyi 你好,咨询一个问题,我看你们线下部署那个,利用scp2将home.html拷贝到指定目录,那线上部署的时候,静态资源应该都部署到CDN了吧,那index.html的又是如何处理的呢,也是在每个服务器上拷贝了一份吗?

不同业务说CSS是怎么合并的啊?

Vue.component(conf.name, (resolve, reject) => {
        loadScript(conf.src).then(() => {
          resolve(modules[id])
        }).catch(() => {
          reject()
        })
      })

想问下

  1. 这里在线上动态注册组件时, conf.src 这个文件应该是组件单独编译打包的成一个文件的吧,所以这里你们是怎么把单独组件编译打包成一个文件的呢?是根据组件配置表遍历后逐个编译?
  2. 本地开发环境 conf.src 这个引用的也是组件编译后的js文件还是类似 .vue 的源码文件?

同求解答,没能想出来各业务线组件打包成 js 后如何被引入,业务线 js 应该如何暴露出来?

@zicong-zhang 业务线打包生成 JS 发布到 cdn,同时更新 JSON 配置文件,主应用的后端负责读取业务线的 JSON 配置文件,然后输出到前端模板中,这样主应用前端就可以知道加载的 JS 地址了。

PS:现在回想自己当年做的这个,不就是现在炒的火热的”微前端”?

20 年再看,这个其实就是「微前端」的早期实践啊