Vue 的多页面配置

在一个 Vue-cli 项目中支持多个页面

有时候,你想开发很多个具备单页应用性质的页面,一个一个用Vue-cli构建并且分成不同的项目未免太蠢了。

这篇文章用于记录我在给VCenter 前端配置多页面的时候的解决方法。

前言

网络上有很多类似的方法,一般都是讲讲增加Entry,增加HtmlWebpackPlugin的相关配置,每次新增或者删除页面的时候都需要做很多工作。

当然,也有一些文章,给了一些自动化解决方案,但是并不美观,也不方便,比如分页面划分文件夹之类的,然后通过webpack配置中的js自动引入这些文件夹,这种就比较倾向于是用文件夹作为约定。

在本文中,我结合了这两种方法,尝试通过一个配置文件来管理自己的多页面方案。

准备工作

首先我们需要一个Vue-cli构建的项目,具备基本的 /build目录,并且有webpack.dev.conf.js 等配置文件。

然后我们需要知道,每一个webpack项目在构建的时候,会发生什么,同时又需要设置哪些配置。

比如说我们需要在 webpack.base.conf.js中添加所有的 Entry(入口文件),每个页面都需要指定那个页面的 main.js,

然后我们还注意到 HtmlWebpackPlugin这个插件,负责将一个HTML 模板,插入指定的经过 webpack处理的js文件。

最后,我们还需要做一些简单的优化工作,控制台打印出所有页面的链接

操作流程

首先,我们的多页配置是基于一个 js 文件,这个js 文件中保存了一个数组,形如:

const path = require('path')

module.exports = [
    {
        name:"app",
        title:"管理后台",
        FileName:"BackEnd.html", //输出的文件名
        OutputFile:path.resolve(__dirname, '../dist/BackEnd.html'),  //输出位置,这个地方需要填写相对路径
        TemplateFile:"index.html", // 模板文件,不出意外的话多个页面可以共享这个文件,问题不大,这个地方的路径填写相对根目录路径,因为使用的时候,那个配置文件有另外的路径检索方式
        Entry:'./src/BackEnd/main.js', // JS入口,路径为相对根目录路径
    },
    {
        name:"CPU_Grade",
        title:"成绩查询",
        FileName:"CPU_Grade.html",
        OutputFile:path.resolve(__dirname, '../dist/CPU_Grade.html'),
        TemplateFile:"index.html",
        Entry:'./src/CPU_Grade/main.js',
    }
]

在这个文件中,我们为每个页面指定了不同的名称(name),title(网页标题),文件名称,输出位置和入口文件路径。
文件名称和输出位置需要一一对应,在这里我直接使用 path 将路径转化了,免得之后再添加类似的代码

然后我们需要对webpack.dev.conf.js进行配置,在plugins属性中,我们需要添加每个页面不同的HtmlWebpackPlugin
这一步我们通过数组map+解构直接插入进去:

    // https://github.com/ampedandwired/html-webpack-plugin
    ...MultiPage.map((Page)=>{
      return new HtmlWebpackPlugin({
        filename: Page.FileName,
        title:Page.title,
        template: Page.TemplateFile,
        inject: true,
        chunks: [Page.name]
      })
    }),
    // copy custom static assets

别忘了要先引入我们的配置文件const MultiPage = require("./MultiPage");

webpack.prod.conf.js里面,也是依样画葫芦,但是这里需要注意,将vendors,manifest和打包的js代码分别引入,因此应该使用chunks相关的代码,当然,也别忘了先引入配置文件

    //生成多页面配置
    ...MultiPage.map((Page)=>{
      return new HtmlWebpackPlugin({
        filename: Page.OutputFile,
        title:Page.title,
        template: Page.TemplateFile,
        inject: true,
        minify: {
          removeComments: true,
          collapseWhitespace: true,
          removeAttributeQuotes: true
          // more options:
          // https://github.com/kangax/html-minifier#options-quick-reference
        },
        // necessary to consistently work with multiple chunks via CommonsChunkPlugin
        chunksSortMode: 'dependency',
        chunks: ['manifest', 'vendor', Page.name]
      })
    }),

以上两处修改都是将原来的HTMLWebpackPlugin替换掉的

最后,我们需要进入webpack.base.conf.js,添加我们的入口文件

const MultiPage = require("./MultiPage");
// 获取Entry的函数
function getEntry(){
  let entry = {};
  MultiPage.forEach((Page)=>{
    entry[Page.name] = Page.Entry;
  })
  return entry;
}


//然后entry应该修改为 
entry: getEntry(),

到了这一步,如果你的配置文件没有写错,目录也没啥问题的话,应该能正常使用webpack了,但是别急,还有一点点小问题

首先需要到你的index.html中将title替换成

    <title><%= htmlWebpackPlugin.options.title%></title>

这样,插件才能正确插入各个页面的 title

另外,你还可以前往webpack.dev.conf.js,在最底部,有一个启动服务器后的提示,在这里可以将所有的文件路径都填写进去,免得手动敲文件名才能调试。代码如下

compilationSuccessInfo: {
          messages: [
            `Your application is running here: http://localhost:${port}`,
            ...MultiPage.map((Page,index)=>{
              return `Page ${index+1}: http://localhost:${port}/${Page.FileName}`
            })
          ],
        },

是的,没错了,也直接使用解构插入进去即可,你可能注意到了,我把原来的devWebpackConfig.devServer.host修改成了localhost,这是因为我的网页需要移动端访问,因此监听了 0.0.0.0 ,方便手机访问,但是电脑上显然只能使用localhost,因此直接替换了。

如果你也想改监听host的话,可以找到/config/index.js,在dev下面做这个修改

host: '0.0.0.0', // can be overwritten by process.env.HOST #这边改成 0.0.0.0来方便监听局域网访问,方便手机端调试

至此,你就可以正常使用Vue项目来构建多页面了,根据我的配置文件,两个html文件都会输出到dist下,同级并列,并且是由同一个index.html模板生成的。

下面放一个最后的效果图:
snipaste_20190105_182944.png

总结

在配置多页面的时候,找了很多的网站和资料,其实完全不需要,只需要认真看一下各个webpack配置文件,捋清楚每一个文件的作用,并且修改配置文件中的特定值为你的自动配置的函数即可。所以还是要多动脑,少照抄。

希望有朝一日有人遇到这个问题的时候这篇文章能够给他一点启发。

Javascript 对象、花括号和冒号

话不多说,先来看一个“表达式”:

{a:1}[123]

猜猜结果?
结果是:

[123]

这个问题不难,但是其中有很多坑,甚至导致半个小时调不出一个bug。

冒号和花括号在Javascript中的作用网上多有文章记载,这里这个表达式之所以能正确解析,是因为将冒号视作了 Label,而且基于优先级,将花括号视作了 复合语句 的标记。那么最后结果是[123]也就不稀奇了,引擎自动为复合语句结束补充了一个分号,毕竟在js里面,分号是可以省略的。

那么其他问题呢?

比如说,我在一个函数传参的时候使用了这样一个"表达式" (严格来讲这已经不是一个表达式了,因为其中被一个不存在的分号分隔开来了)

那么我们直接在外面包一个括号,将其作为语句执行,结果是这样的:

({a:1}[123])  === "undefined"     //true

这个结果是比较出人意料的,我只能解释成 js引擎在解析的时候,语法树中明明是一个表达式,结果却被补充了一个分号,导致无法正确的解析从而返回了一个 "undefined",这么说的话,简单一点, ({}[1]) 也应该是 "undefined"。事实也正是如此。

那么这个坑会有什么影响呢?举个栗子,你在传参的时候,使用对象字面量表达式,后面又跟了一个数组字面量表达式,如果恰好漏了一个逗号,那么原本应该接收到的参数,就变成了一个undefined,偏偏这又不会报错,从语法分析上来讲,所有的语句都是合法的。

function log(...args){
    console.log("收到的参数有:",...args)
}

log({a:1}[123],"123")

// 输出:  收到的参数有: undefined 123

别问,问就是今天下午调这个 bug 调了半个小时

Tampermonkey 使用本地编辑器开发的流程

关于Tampermonkey 使用本地编辑器开发的一些准备工作

在Tampermonkey 提供的编辑器里面进行编码实在是太劳累了,于是找了找使用本地编辑器编写代码的方法,在这里做一下总结。

打开各项设置

在Tampermonkey 设置中,打开原生导入脚本

snipaste_20181222_203858.png

然后这个时候应该会显示一个warning,提示需要浏览器允许插件访问文件网址

这一步直接右键菜单栏的Tampermonkey,选择“管理扩展内容”,然后找到“允许访问文件网址”即可。

在第三项,还要求提供“浏览器配置文件路径”,这个也不难找。win+e打开Exploreralt+d进入地址栏,输入

%userProfile%/AppData/Local/

然后找到自己当前使用的浏览器的哪个文件夹,比如我当前使用的CentBrowser,Chrome应该是Chrome

然后找到浏览器文件夹下的 \User Data\Default\Extensions\

到这一步会出现一个有很多Hash过的文件夹名,这个不用着急,还是到浏览器,右键插件图标,点击Tampermonkey,会跳转到谷歌应用商店,这个时候地址栏最后的一串就是你要找的那个Hash了。

这个时候,如果你没有关闭地址栏完全路径展示的话,你可以点击一下地址栏空白处,最终应当填进去的路径应该是

C:\Users\<UserName>\AppData\Local\CentBrowser\User Data\Default\Extensions\dhdgffkkebhmkfjojejmpbldmpobfkfo

到这个时候,Tampermonkey访问本地文件已经没有其他缺陷了。

使用方式

随便在哪里新建一个.js文件,然后再新建一个脚本,在脚本Header中添加

// @require    file:// <-path-> 

<-path-> 应当是这个文件的绝对路径,例如 D:\a.js,然后保存该脚本

这个时候你使用本地编辑器编辑完毕 a.js 的时候,刷新网页,就可以看到Tampermonkey正常require本地文件了。

注意,在发布的时候,一定要把require本地文件的代码删掉,把本地的代码复制到脚本文件中

总结

这只是一个简单的小经验,其实还可以做更多的扩展,比如做一些 Webpack 之类的,让require自动监听指定的文件,甚至使用webpack 自动刷新网页,搜了一下,有类似的webpack插件,名字叫做webpack-tampermonkey,看介绍仅仅是将代码整合成.user.js

Vue使用CDN减小打包体积

Vue 使用 CDN 减少打包体积

详细步骤

具体的步骤无需赘述了,index.html之中引入 CDN 的 script

    <!-- axios -->
    <script src="https://cdn.bootcss.com/axios/0.19.0-beta.1/axios.min.js"></script>
    <!-- Vue -->
    <script src="https://cdn.bootcss.com/vue/2.5.17/vue.min.js"></script>
    <!-- VueRouter -->
    <script src="https://cdn.bootcss.com/vue-router/3.0.2/vue-router.min.js"></script>
    <!-- ElementUI -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/element-ui/2.4.11/index.js"></script>
    <link href="https://cdnjs.cloudflare.com/ajax/libs/element-ui/2.4.11/theme-chalk/index.css" rel="stylesheet"/>

第二步在webpack里面配置externals:

  entry: {
    app: './src/main.js'
  },
  externals:{
    'vue': 'Vue',
    'vue-router': 'VueRouter',
    "axios":"axios",
    "ElementUI":"ELEMENT",
  },

最后从组件中删除 import xxx from vue之类的即可。
事实上不删也无所谓,webpack打包时自会略去这一部分(前提是与external中配置的名称一致)。

备用CDN

其实我想讲的是第二部分,书中(网上)只提使用CDN的方法,却对CDN故障以后的备用处理语焉不详。

查得一些处理方法,甚至有说利用 requireJs 来加载的,若是这样的话岂不是与使用CDN的初衷相悖了?

略微思考了一下,可以参照其他文章的 document.write 的方法进行处理

在这里我的处理方法是这样的

<script>
      /* Catch CDN Failed  */
      /* Vue */
      window.Vue || document.write('<script src="https://cdn.jsdelivr.net/npm/vue@2.5.18/dist/vue.min.js"><\/script>')
      /* axios */
      window.axios || document.write('<script src="https://cdn.jsdelivr.net/npm/axios@0.18.0/dist/axios.min.js"><\/script>')
      /* VueRouter */
      window.VueRouter || document.write('<script src="https://cdn.jsdelivr.net/npm/vue-router@3.0.2/dist/vue-router.min.js"><\/script>')
      /* Element */
      window.ELEMENT || document.write('<script src="https://cdn.jsdelivr.net/npm/element-ui@1.3.1/lib/index.js"><\/script>')
    </script>

对于 style 文件,需要提前配置,由于无法同步读取style文件的状态,onload事件又是异步的,因此通过onerror事件,提前指定处理函数即可。

    <script>
      /* Element_CSS */
      function getElementStyle(){
        let link = document.createElement('link');
        link.href = "https://cdn.jsdelivr.net/npm/element-ui@1.3.1/lib/theme-default/index.css";
        link.rel = "stylesheet"
        document.querySelector('script').parentNode.appendChild(link);
      }
    </script>

    <link href="https://cdnjs.cloudflare.com/ajax/libs/element-ui/2.4.11/theme-chalk/index.css" rel="stylesheet" onerror="getElementStyle()">

要注意的是,这里不可以使用document.write,因为文档大概率已经渲染完毕,使用write会直接清空当前文档,所以必须使用appendChild。


不久之前的 BootCDN 事件远近闻名,因此经过了一番考虑之后,才有了现在这篇总结

KOA与stream的一个坑

绝对不要使用 ctx.body = request.get(stream_link)

在我的一个本地Server中,制作了一个解析视频网站视频流的API

大概的形式是这样的,

由 nodejs 向 视频网站请求流,然后使用ctx.body = request.get(stream_link)的方式向本地播放器输出流。

但是这其中有一个非常大的问题,就是在关闭播放器,KOA也获取到 ECONNRESET 错误的情况下,无法 destory Request的流。

这就导致即使关闭了播放器,nodejs依然在下载对应的那个视频,如果多切换几个视频,后台就会有多个下载任务占用带宽。

找了半天没有找到处理办法,KOA的相关讨论也只是说尽量不要给ctx.body设置为HTTP流。

最后直接Hack了一下,在app上直接监听Error事件,发生ECONNRESET的时候直接process.exit(0),反正后台有 forever的Daemon。

至少目前还没有找到可靠的处理方法。

如果可以处理的话,思路大概这样,将HTTP stream的引用暂存起来,读取ECONNRESET,并且与对应的ctx对应,找到 stream ,想办法结束流。

这个就留待以后实现吧,仅在此做一个记录