基於Vue.js的大型報告頁專案實現過程及問題總結(一)

李文楊發表於2017-11-02

今年5月份的時候做了一個測評報告專案,需要在網頁正常顯示的同時且可列印為pdf,當時的技術方案採用jquery+template的方式,因為是固定模板所以並沒有考慮報告的模組化區分,九月底產品提出新的需求,由於報告頁數動輒上千頁,所以希望使用者自行選擇內容生成報告,這個時候原專案就不夠靈活了,與小夥伴商量決定將這個專案使用vue進行重構,對報告模組進行細分封裝元件複用,大概一個月的工期,中途遇到n多坑,趁著今天有時間將實現思路整理出來並將出現的問題總結一下

整體的實現思維導圖如下:

需要考慮的:

1.可生成PDF版且可列印

2.根據後臺獲取的json生成包含相應模組的報告

3.元件內基於echarts封裝圖表的引用

4.目錄模組的頁碼定位

5.如何進行模組內的細分(如1.2.1.3);

6.webpack對多頁面編譯的配置

 

Ps:轉PDF外掛使用的是OpenHtmlToPdf具體配置方法可自行百度,在這裡不過多贅述。

關於pdf的一點小坑(知識點朋友們!):

網頁列印A4紙的尺寸是(1123*793),在使用OpenHtmlToPdf時無法使用css3百分之八十的屬性,像translate等,還有就是margin-top不會生效,使用padding-top代替吧,列印生無法請求ajax,如需列印請將資料先儲存到本地再行列印,可根據不同瀏覽方式判斷兩種方案。

以下實現全部是基於Vue-cli快速構建的專案中實現的,vue-cli的安裝網上有很多詳細的教程不過多說了

1.新建專案,命令列執行程式碼:

vue init webpack vuetest

命令輸入後,會進入安裝階段,需要使用者輸入一些資訊

Project name (vuetest)                    專案名稱,可以自己指定,也可直接回車,按照括號中預設名字(注意這裡的名字不能有大寫字母,如果有會報錯Sorry, name can no longer contain capital letters),阮一峰老師部落格為什麼檔名要小寫 ,可以參考一下。

Project description (A Vue.js project)  專案描述,也可直接點選回車,使用預設名字

Author (........)       作者,不用說了,你想輸什麼就輸什麼吧

接下來會讓使用者選擇

Runtime + Compiler: recommended for most users    執行加編譯,既然已經說了推薦,就選它了

Runtime-only: about 6KB lighter min+gzip, but templates (or any Vue-specificHTML) are ONLY allowed in .vue files - render functions are required elsewhere   僅執行時,已經有推薦了就選擇第一個了

Install vue-router? (Y/n)    是否安裝vue-router,這是官方的路由,大多數情況下都使用,vue-router官網 。這裡就輸入“y”後回車即可。

Use ESLint to lint your code? (Y/n)      是否使用ESLint管理程式碼,ESLint是個程式碼風格管理工具,是用來統一程式碼風格的,並不會影響整體的執行,這也是為了多人協作,新手就不用了,一般專案中都會使用。ESLint官網

接下來也是選擇題Pick an ESLint preset (Use arrow keys)            選擇一個ESLint預設,編寫vue專案時的程式碼風格,因為我選擇了使用ESLint

Standard (https://github.com/feross/standard)    標準,有些看不明白,什麼標準呢,去給提示的standardgithub地址看一下, 原來時js的標準風格

AirBNB (https://github.com/airbnb/javascript)    JavaScript最合理的方法,這個github地址說的是JavaScript最合理的方法

none (configure it yourself)    這個不用說,自己定義風格

具體選擇哪個因人而異吧  ,我選擇標準風格

Setup unit tests with Karma + Mocha? (Y/n)  是否安裝單元測試,我選擇安裝

Setup e2e tests with Nightwatch(Y/n)?     是否安裝e2e測試 ,我選擇安裝

完成

 初始的目錄結構大概是這樣的

由於是多頁面應用所以需要在src下建一個modle資料夾裡面是兩個不同的專案

 

注意:

 這裡的index.html是入口檔案,一定不能少,這這裡做中轉預設進入demo1的頁面

<body>
    <script>
        location.href = "module/demo1.html";
    </script>
</body>

下面對多頁面進行配置,主要操作config和build這兩個資料夾


/build
    build.js               #構建生產程式碼
    dev-client.js 
    dev-server.js          #執行本地伺服器
    utils.js               #額外的通用方法
    webpack.base.conf.js   #預設的webpack配置
    webpack.dev.conf.js    #本地開發的webpack配置
    webpack.prod.conf.js   #構建生產的webpack配置
/config   配置檔案
    dev.env.js
    index.js
    pord.env.js
    test.env.js
/src
    assets                  #放資源
    components              #元件
    /module                 #頁面模組
        /home               #子頁面
            index.html      #模版頁面
            index.js        #js入口
        // 注意,這裡的html和js的檔名要一致,如上面就是index    
/dist                       #最後打包生成的資源
    /js                
    /css
    /home
 

修改預設的webpack配置webpack.base.conf.js

生成需要的入口檔案

var path = require('path')
var config = require('../config')
var utils = require('./utils')
var projectRoot = path.resolve(__dirname, '../')
var glob = require('glob');
var entries = getEntry(['./src/demo1/index/*.js', './src/module/demo2/*.js']); // 獲得入口js檔案

var env = process.env.NODE_ENV
// check env & config/index.js to decide weither to enable CSS Sourcemaps for the
// various preprocessor loaders added to vue-loader at the end of this file
var cssSourceMapDev = (env === 'development' && config.dev.cssSourceMap)
var cssSourceMapProd = (env === 'production' && config.build.productionSourceMap)
var useCssSourceMap = cssSourceMapDev || cssSourceMapProd

module.exports = {
entry: entries,
output: {
path: config.build.assetsRoot,
publicPath: process.env.NODE_ENV === 'production' ? config.build.assetsPublicPath : config.dev.assetsPublicPath,
filename: '[name].js'
},
resolve: {
extensions: ['', '.js', '.vue','.json'],

fallback: [path.join(__dirname, '../node_modules')],
alias: {
'vue$': 'vue/dist/vue',
'src': path.resolve(__dirname, '../src'),
'common': path.resolve(__dirname, '../src/common'),
'components': path.resolve(__dirname, '../src/components')
}
},
resolveLoader: {
fallback: [path.join(__dirname, '../node_modules')]
},
module: {
loaders: [{
test: /\.vue$/,
loader: 'vue'
},
{
test: /\.js$/,
loader: 'babel',
include: projectRoot,
exclude: /node_modules/
},
{
test: /\.json$/,
loader: 'json'
},
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
loader: 'url',
query: {
limit: 10000,
name: utils.assetsPath('img/[name].[hash:7].[ext]')
}
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
loader: 'url',
query: {
limit: 10000,
name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
}
}
]
},
vue: {
loaders: utils.cssLoaders({
sourceMap: useCssSourceMap
}),
postcss: [
require('autoprefixer')({
browsers: ['last 2 versions']
})
]
}
}

function getEntry(globPath) {
var entries = {},
basename, tmp, pathname;
if (typeof (globPath) != "object") {
globPath = [globPath]
}
globPath.forEach((itemPath) => {
glob.sync(itemPath).forEach(function (entry) {
basename = path.basename(entry, path.extname(entry));
if (entry.split('/').length > 4) {
tmp = entry.split('/').splice(-3);
pathname = tmp.splice(0, 1) + '/' + basename; // 正確輸出js和html的路徑
entries[pathname] = entry;
} else {
entries[basename] = entry;
}
});
});
return entries;
}

修改本地開發的webpack配置webpack.dev.conf.js

這裡是和本地伺服器有關的配置

這裡是根據目錄生成對應的頁面

var path = require('path');
var config = require('../config')
var webpack = require('webpack')
var merge = require('webpack-merge')
var utils = require('./utils')
var baseWebpackConfig = require('./webpack.base.conf')
var HtmlWebpackPlugin = require('html-webpack-plugin')
var glob = require('glob')
// add hot-reload related code to entry chunks
Object.keys(baseWebpackConfig.entry).forEach(function (name) {
baseWebpackConfig.entry[name] = ['./build/dev-client'].concat(baseWebpackConfig.entry[name])
})
module.exports = merge(baseWebpackConfig, {
module: {
loaders: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap })
},
// eval-source-map is faster for development
devtool: '#eval-source-map',
plugins: [
new webpack.DefinePlugin({
'process.env': config.dev.env
}),
// https://github.com/glenjamin/webpack-hot-middleware#installation--usage
new webpack.optimize.OccurenceOrderPlugin(),
new webpack.HotModuleReplacementPlugin(),
new webpack.NoErrorsPlugin()
]
})

function getEntry(globPath) {
var entries = {},
basename, tmp, pathname;
if (typeof (globPath) != "object") {
globPath = [globPath]
}
globPath.forEach((itemPath) => {
glob.sync(itemPath).forEach(function (entry) {
basename = path.basename(entry, path.extname(entry));
if (entry.split('/').length > 4) {
tmp = entry.split('/').splice(-3);
pathname = tmp.splice(0, 1) + '/' + basename; // 正確輸出js和html的路徑
entries[pathname] = entry;
} else {
entries[basename] = entry;
}
});
});
return entries;
}

var pages = getEntry(['./src/module/*.html','./src/module/**/*.html']);

for (var pathname in pages) {
// 配置生成的html檔案,定義路徑等
var conf = {
filename: pathname + '.html',
template: pages[pathname], // 模板路徑
inject: true, // js插入位置
// necessary to consistently work with multiple chunks via CommonsChunkPlugin
chunksSortMode: 'dependency'

};

if (pathname in module.exports.entry) {
conf.chunks = ['manifest', 'vendor', pathname];
conf.hash = true;
}

module.exports.plugins.push(new HtmlWebpackPlugin(conf));
}

修改構建生產的webpack配置webpack.prod.conf.js

var path = require('path')
var config = require('../config')
var utils = require('./utils')
var webpack = require('webpack')
var merge = require('webpack-merge')
var baseWebpackConfig = require('./webpack.base.conf')
var ExtractTextPlugin = require('extract-text-webpack-plugin')
var HtmlWebpackPlugin = require('html-webpack-plugin')
var CleanPlugin = require('clean-webpack-plugin')//webpack外掛,用於清除目錄檔案
var glob = require('glob');
var env = config.build.env

var webpackConfig = merge(baseWebpackConfig, {
  module: {
    loaders: utils.styleLoaders({ sourceMap: config.build.productionSourceMap, extract: true })
  },
  devtool: config.build.productionSourceMap ? '#source-map' : false,
  output: {
    path: config.build.assetsRoot,
    filename: utils.assetsPath('js/[name].[chunkhash].js'),
    chunkFilename: utils.assetsPath('js/[id].[chunkhash].js')
  },
  vue: {
    loaders: utils.cssLoaders({
      sourceMap: config.build.productionSourceMap,
      extract: true
    })
  },
  plugins: [
    // http://vuejs.github.io/vue-loader/workflow/production.html
    new webpack.DefinePlugin({
      'process.env': env
    }),
    new webpack.optimize.UglifyJsPlugin({
      compress: {
        warnings: false
      }
    }),
    new CleanPlugin(['../dist']), //清空生成目錄
    new webpack.optimize.OccurenceOrderPlugin(),
    // extract css into its own file
    new ExtractTextPlugin(utils.assetsPath('css/[name].[contenthash].css')),
    // generate dist index.html with correct asset hash for caching.
    // you can customize output by editing /index.html
    // see https://github.com/ampedandwired/html-webpack-plugin
    // split vendor js into its own file
    new webpack.optimize.CommonsChunkPlugin({
      name: 'vendor',
      minChunks: function (module, count) {
        // any required modules inside node_modules are extracted to vendor
        return (
          module.resource &&
          /\.js$/.test(module.resource) &&
          module.resource.indexOf(
            path.join(__dirname, '../node_modules')
          ) === 0
        )
      }
    }),
    // extract webpack runtime and module manifest to its own file in order to
    // prevent vendor hash from being updated whenever app bundle is updated
    new webpack.optimize.CommonsChunkPlugin({
      name: 'manifest',
      chunks: ['vendor']
    })
  ]
})

if (config.build.productionGzip) {
  var CompressionWebpackPlugin = require('compression-webpack-plugin')

  webpackConfig.plugins.push(
    new CompressionWebpackPlugin({
      asset: '[path].gz[query]',
      algorithm: 'gzip',
      test: new RegExp(
        '\\.(' +
        config.build.productionGzipExtensions.join('|') +
        ')$'
      ),
      threshold: 10240,
      minRatio: 0.8
    })
  )
}

module.exports = webpackConfig

function getEntry(globPath) {
  var entries = {},
    basename, tmp, pathname;
  if (typeof (globPath) != "object") {
    globPath = [globPath]
  }
  globPath.forEach((itemPath) => {
    glob.sync(itemPath).forEach(function (entry) {
      basename = path.basename(entry, path.extname(entry));
      if (entry.split('/').length > 4) {
        tmp = entry.split('/').splice(-3);
        pathname = tmp.splice(0, 1) + '/' + basename; // 正確輸出js和html的路徑
        entries[pathname] = entry;
      } else {
        entries[basename] = entry;
      }
    });
  });
  return entries;
}

var pages = getEntry(['./src/module/*.html','./src/module/**/*.html']);

for (var pathname in pages) {
  // 配置生成的html檔案,定義路徑等
  var conf = {
    filename: pathname + '.html',
    template: pages[pathname],   // 模板路徑
    inject: true,              // js插入位置
    // necessary to consistently work with multiple chunks via CommonsChunkPlugin
    chunksSortMode: 'dependency'
  };

  if (pathname in module.exports.entry) {
    conf.chunks = ['manifest', 'vendor', pathname];
    conf.hash = true;
  }

  module.exports.plugins.push(new HtmlWebpackPlugin(conf));
}
var path = require('path')
var config = require('../config')
var utils = require('./utils')
var webpack = require('webpack')
var merge = require('webpack-merge')
var baseWebpackConfig = require('./webpack.base.conf')
var ExtractTextPlugin = require('extract-text-webpack-plugin')
var HtmlWebpackPlugin = require('html-webpack-plugin')
var CleanPlugin = require('clean-webpack-plugin')//webpack外掛,用於清除目錄檔案
var glob = require('glob');
var env = config.build.env

var webpackConfig = merge(baseWebpackConfig, {
  module: {
    loaders: utils.styleLoaders({ sourceMap: config.build.productionSourceMap, extract: true })
  },
  devtool: config.build.productionSourceMap ? '#source-map' : false,
  output: {
    path: config.build.assetsRoot,
    filename: utils.assetsPath('js/[name].[chunkhash].js'),
    chunkFilename: utils.assetsPath('js/[id].[chunkhash].js')
  },
  vue: {
    loaders: utils.cssLoaders({
      sourceMap: config.build.productionSourceMap,
      extract: true
    })
  },
  plugins: [
    // http://vuejs.github.io/vue-loader/workflow/production.html
    new webpack.DefinePlugin({
      'process.env': env
    }),
    new webpack.optimize.UglifyJsPlugin({
      compress: {
        warnings: false
      }
    }),
    new CleanPlugin(['../dist']), //清空生成目錄
    new webpack.optimize.OccurenceOrderPlugin(),
    // extract css into its own file
    new ExtractTextPlugin(utils.assetsPath('css/[name].[contenthash].css')),
    // generate dist index.html with correct asset hash for caching.
    // you can customize output by editing /index.html
    // see https://github.com/ampedandwired/html-webpack-plugin
    // split vendor js into its own file
    new webpack.optimize.CommonsChunkPlugin({
      name: 'vendor',
      minChunks: function (module, count) {
        // any required modules inside node_modules are extracted to vendor
        return (
          module.resource &&
          /\.js$/.test(module.resource) &&
          module.resource.indexOf(
            path.join(__dirname, '../node_modules')
          ) === 0
        )
      }
    }),
    // extract webpack runtime and module manifest to its own file in order to
    // prevent vendor hash from being updated whenever app bundle is updated
    new webpack.optimize.CommonsChunkPlugin({
      name: 'manifest',
      chunks: ['vendor']
    })
  ]
})

if (config.build.productionGzip) {
  var CompressionWebpackPlugin = require('compression-webpack-plugin')

  webpackConfig.plugins.push(
    new CompressionWebpackPlugin({
      asset: '[path].gz[query]',
      algorithm: 'gzip',
      test: new RegExp(
        '\\.(' +
        config.build.productionGzipExtensions.join('|') +
        ')$'
      ),
      threshold: 10240,
      minRatio: 0.8
    })
  )
}

module.exports = webpackConfig

function getEntry(globPath) {
  var entries = {},
    basename, tmp, pathname;
  if (typeof (globPath) != "object") {
    globPath = [globPath]
  }
  globPath.forEach((itemPath) => {
    glob.sync(itemPath).forEach(function (entry) {
      basename = path.basename(entry, path.extname(entry));
      if (entry.split('/').length > 4) {
        tmp = entry.split('/').splice(-3);
        pathname = tmp.splice(0, 1) + '/' + basename; // 正確輸出js和html的路徑
        entries[pathname] = entry;
      } else {
        entries[basename] = entry;
      }
    });
  });
  return entries;
}

var pages = getEntry(['./src/module/*.html','./src/module/**/*.html']);

for (var pathname in pages) {
  // 配置生成的html檔案,定義路徑等
  var conf = {
    filename: pathname + '.html',
    template: pages[pathname],   // 模板路徑
    inject: true,              // js插入位置
    // necessary to consistently work with multiple chunks via CommonsChunkPlugin
    chunksSortMode: 'dependency'
  };

  if (pathname in module.exports.entry) {
    conf.chunks = ['manifest', 'vendor', pathname];
    conf.hash = true;
  }

  module.exports.plugins.push(new HtmlWebpackPlugin(conf));
}

修改配置檔案config

修改index.js
在build.js中會引用assetsRoot,這裡就是對應的根目錄,改成你想要輸出的地址就好了。ps:這裡是相對地址
assetsPublicPath會被引用插入到頁面的模版中,這個是你資源的根目錄
// see http://vuejs-templates.github.io/webpack for documentation.
var path = require('path')

module.exports = {
  build: {
    env: require('./prod.env'),
    index: path.resolve(__dirname, '../dist/index.html'),
    assetsRoot: path.resolve(__dirname, '../dist'),
    assetsSubDirectory: 'static',
    assetsPublicPath: '../',
    productionSourceMap: true,
    // Gzip off by default as many popular static hosts such as
    // Surge or Netlify already gzip all static assets for you.
    // Before setting to `true`, make sure to:
    // npm install --save-dev compression-webpack-plugin
    productionGzip: false,
    productionGzipExtensions: ['js', 'css']
  },
  dev: {
    env: require('./dev.env'),
    port: 8080,
    assetsSubDirectory: 'static',
    assetsPublicPath: '/',
    proxyTable: {},
    // CSS Sourcemaps off by default because relative paths are "buggy"
    // with this option, according to the CSS-Loader README
    // (https://github.com/webpack/css-loader#sourcemaps)
    // In our experience, they generally work as expected,
    // just be aware of this issue when enabling this option.
    cssSourceMap: false
  }
}

ok,配置結束,一個基本的多頁面應用已經成功建成

接下來就進入正題了,放在下一篇來寫。。。。。。。

相關文章