github地址(內含簡單例子)
使用技術棧
webpack(^2.6.1)
webpack-dev-server(^2.4.5)
vue(^2.3.3)
vuex(^2.3.1)
vue-router(^2.5.3)
vue-loader(^12.2.1)
eslint(^3.19.0)
需要學習的知識
vue.js
vuex
vue-router
vue-loader
webpack2
eslint
內容相當多,尤其是webpack2教程,官方腳手架vue-cli雖然相當完整齊全,但是修改起來還是挺花時間,於是自己參照網上的資料和之前做過的項目用到的構建工具地去寫了一個簡單vue項目腳手架。適用於多頁面spa模式的業務場景(每個模塊都是一個spa)。比較簡單,主要就是一個webpack.config.js文件,沒有說特意地去划分成分webpack.dev.config.js、webpack.prov.config.js等等。下面是整個webpack.config.js文件代碼:
1 const { resolve } = require('path') 2 const webpack = require('webpack') 3 const HtmlWebpackPlugin = require('html-webpack-plugin') 4 const ExtractTextPlugin = require('extract-text-webpack-plugin') 5 const glob = require('glob') 6 7 module.exports = (options = {}) => { 8 // 配置文件,根據 run script不同的config參數來調用不同config 9 const config = require('./config/' + (process.env.npm_config_config || options.config || 'dev')) 10 // 遍歷入口文件,這里入口文件與模板文件名字保持一致,保證能同時合成HtmlWebpackPlugin數組和入口文件數組 11 const entries = glob.sync('./src/modules/*.js') 12 const entryJsList = {} 13 const entryHtmlList = [] 14 for (const path of entries) { 15 const chunkName = path.slice('./src/modules/'.length, -'.js'.length) 16 entryJsList[chunkName] = path 17 entryHtmlList.push(new HtmlWebpackPlugin({ 18 template: path.replace('.js', '.html'), 19 filename: 'modules/' + chunkName + '.html', 20 chunks: ['manifest', 'vendor', chunkName] 21 })) 22 } 23 // 處理開發環境和生產環境ExtractTextPlugin的使用情況 24 function cssLoaders(loader, opt) { 25 const loaders = loader.split('!') 26 const opts = opt || {} 27 if (options.dev) { 28 if (opts.extract) { 29 return loader 30 } else { 31 return loaders 32 } 33 } else { 34 const fallbackLoader = loaders.shift() 35 return ExtractTextPlugin.extract({ 36 use: loaders, 37 fallback: fallbackLoader 38 }) 39 } 40 } 41 42 const webpackObj = { 43 entry: Object.assign({ 44 vendor: ['vue', 'vuex', 'vue-router'] 45 }, entryJsList), 46 // 文件內容生成哈希值chunkhash,使用hash會更新所有文件 47 output: { 48 path: resolve(__dirname, 'dist'), 49 filename: options.dev ? 'static/js/[name].js' : 'static/js/[name].[chunkhash].js', 50 chunkFilename: 'static/js/[id].[chunkhash].js', 51 publicPath: config.publicPath 52 }, 53 54 externals: { 55 56 }, 57 58 module: { 59 rules: [ 60 // 只 lint 本地 *.vue 文件,需要安裝eslint-plugin-html,並配置eslintConfig(package.json) 61 { 62 enforce: 'pre', 63 test: /.vue$/, 64 loader: 'eslint-loader', 65 exclude: /node_modules/ 66 }, 67 /* 68 http://blog.guowenfh.com/2016/08/07/ESLint-Rules/ 69 http://eslint.cn/docs/user-guide/configuring 70 [eslint資料] 71 */ 72 { 73 test: /\.js$/, 74 exclude: /node_modules/, 75 use: ['babel-loader', 'eslint-loader'] 76 }, 77 // 需要安裝vue-template-compiler,不然編譯報錯 78 { 79 test: /\.vue$/, 80 loader: 'vue-loader', 81 options: { 82 loaders: { 83 sass: cssLoaders('vue-style-loader!css-loader!sass-loader', { extract: true }) 84 } 85 } 86 }, 87 { 88 // 需要有相應的css-loader,因為第三方庫可能會有文件 89 // (如:element-ui) css在node_moudle 90 // 生產環境才需要code抽離,不然的話,會使熱重載失效 91 test: /\.css$/, 92 use: cssLoaders('style-loader!css-loader') 93 }, 94 { 95 test: /\.(scss|sass)$/, 96 use: cssLoaders('style-loader!css-loader!sass-loader') 97 }, 98 { 99 test: /\.(png|jpg|jpeg|gif|eot|ttf|woff|woff2|svg|svgz)(\?.+)?$/, 100 use: [ 101 { 102 loader: 'url-loader', 103 options: { 104 limit: 10000, 105 name: 'static/imgs/[name].[ext]?[hash]' 106 } 107 } 108 ] 109 } 110 ] 111 }, 112 113 plugins: [ 114 ...entryHtmlList, 115 // 抽離css 116 new ExtractTextPlugin({ 117 filename: 'static/css/[name].[chunkhash].css', 118 allChunks: true 119 }), 120 // 抽離公共代碼 121 new webpack.optimize.CommonsChunkPlugin({ 122 names: ['vendor', 'manifest'] 123 }), 124 // 定義全局常量 125 // cli命令行使用process.env.NODE_ENV不如期望效果,使用不了,所以需要使用DefinePlugin插件定義,定義形式'"development"'或JSON.stringify('development') 126 new webpack.DefinePlugin({ 127 'process.env': { 128 NODE_ENV: options.dev ? JSON.stringify('development') : JSON.stringify('production') 129 } 130 }) 131 132 ], 133 134 resolve: { 135 // require時省略的擴展名,不再需要強制轉入一個空字符串,如:require('module') 不需要module.js 136 extensions: ['.js', '.json', '.vue', '.scss', '.css'], 137 // require路徑簡化 138 alias: { 139 '~': resolve(__dirname, 'src'), 140 // Vue 最早會打包生成三個文件,一個是 runtime only 的文件 vue.common.js,一個是 compiler only 的文件 compiler.js,一個是 runtime + compiler 的文件 vue.js。 141 // vue.js = vue.common.js + compiler.js,默認package.json的main是指向vue.common.js,而template 屬性的使用一定要用compiler.js,因此需要在alias改變vue指向 142 vue: 'vue/dist/vue' 143 }, 144 // 指定import從哪個目錄開始查找 145 modules: [ 146 resolve(__dirname, 'src'), 147 'node_modules' 148 ] 149 }, 150 // 開啟http服務,publicPath => 需要與Output保持一致 || proxy => 反向代理 || port => 端口號 151 devServer: config.devServer ? { 152 port: config.devServer.port, 153 proxy: config.devServer.proxy, 154 publicPath: config.publicPath, 155 stats: { colors: true } 156 } : undefined, 157 // 屏蔽文件超過限制大小的warn 158 performance: { 159 hints: options.dev ? false : 'warning' 160 }, 161 // 生成devtool,保證在瀏覽器可以看到源代碼,生產環境設為false 162 devtool: 'inline-source-map' 163 } 164 165 if (!options.dev) { 166 webpackObj.devtool = false 167 webpackObj.plugins = (webpackObj.plugins || []).concat([ 168 // 壓縮js 169 new webpack.optimize.UglifyJsPlugin({ 170 // webpack2,默認為true,可以不用設置 171 compress: { 172 warnings: false 173 } 174 }), 175 // 壓縮 loaders 176 new webpack.LoaderOptionsPlugin({ 177 minimize: true 178 }) 179 ]) 180 } 181 182 return webpackObj 183 }
上面的代碼對於每個配置項都有注釋說明,這里有幾點需要注意的:
1. webpack.config.js導出的是一個function
之前項目的webpack.config.js是以對象形式export的,如下
1 module.exports = { 2 entry: ..., 3 output: { 4 ... 5 }, 6 ... 7 }
而現在倒出來的是一個function,如下:
1 module.exports = (options = {}) => { 2 return { 3 entry: ..., 4 output: { 5 ... 6 }, 7 ... 8 } 9 }
這樣的話,function會在執行webpack CLI的時候獲取webpack的參數,通過options傳進function,看一下package.json:
1 "local": "npm run dev --config=local", 2 "dev": "webpack-dev-server -d --hot --inline --env.dev --env.config dev", 3 "build": "rimraf dist && webpack -p --env.config prod" //rimraf清空dist目錄
對於local命令,我們執行的是dev命令,但是在最后面會--config=local,這是配置,這樣我們可以通過process.env.npm_config_config獲取到,而對於dev命令,對於--env XXX,我們便可以在function獲取option.config= 'dev' 和 option.dev= true的值,特別方便!以此便可以同步參數來加載不同的配置文件了。對於-d、-p不清楚的話,可以這里查看,很詳細!
1 // 配置文件,根據 run script不同的config參數來調用不同config 2 const config = require('./config/' + (process.env.npm_config_config || options.config || 'dev'))
2. modules放置模板文件、入口文件、對應模塊的vue文件
將入口文件和模板文件放到modules目錄(名字保持一致),webpack文件會通過glob讀取modules目錄,遍歷生成入口文件對象和模板文件數組,如下:
1 const entries = glob.sync('./src/modules/*.js') 2 const entryJsList = {} 3 const entryHtmlList = [] 4 for (const path of entries) { 5 const chunkName = path.slice('./src/modules/'.length, -'.js'.length) 6 entryJsList[chunkName] = path 7 entryHtmlList.push(new HtmlWebpackPlugin({ 8 template: path.replace('.js', '.html'), 9 filename: 'modules/' + chunkName + '.html', 10 chunks: ['manifest', 'vendor', chunkName] 11 })) 12 }
對於HtmlWebpackPlugin插件中幾個配置項的意思是,template:模板路徑,filename:文件名稱,這里為了區分開來模板文件我是放置在dist/modules文件夾中,而對應的編譯打包好的js、img(對於圖片我們是使用file-loader、url-loader進行抽離,對於這兩個不是很理解的,可以看這里)、css我也是會放在dist/下對應目錄的,這樣目錄會比較清晰。chunks:指定插入文件中的chunk,后面我們會生成manifest文件、公共vendor、以及對應生成的jscss(名稱一樣)
3. 處理開發環境和生產環境ExtractTextPlugin的使用情況
開發環境,不需要把css進行抽離,要以style插入html文件中,可以很好實現熱替換 生產環境,需要把css進行抽離合並,如下(根據options.dev區分開發和生產):
1 // 處理開發環境和生產環境ExtractTextPlugin的使用情況 2 function cssLoaders(loader, opt) { 3 const loaders = loader.split('!') 4 const opts = opt || {} 5 if (options.dev) { 6 if (opts.extract) { 7 return loader 8 } else { 9 return loaders 10 } 11 } else { 12 const fallbackLoader = loaders.shift() 13 return ExtractTextPlugin.extract({ 14 use: loaders, 15 fallback: fallbackLoader 16 }) 17 } 18 } 19 ... 20 // 使用情況 21 // 注意:需要安裝vue-template-compiler,不然編譯會報錯 22 { 23 test: /\.vue$/, 24 loader: 'vue-loader', 25 options: { 26 loaders: { 27 sass: cssLoaders('vue-style-loader!css-loader!sass-loader', { extract: true }) 28 } 29 } 30 }, 31 ... 32 { 33 test: /\.(scss|sass)$/, 34 use: cssLoaders('style-loader!css-loader!sass-loader') 35 }
再使用ExtractTextPlugin合並抽離到static/css/目錄
4. 定義全局常量
cli命令行(webpack -p)使用process.env.NODE_ENV不如期望效果,使用不了,所以需要使用DefinePlugin插件定義,定義形式'"development"'或JSON.stringify(process.env.NODE_ENV),我使用這樣的寫法'development',結果報錯(針對webpack2),查找了一下網上資料,它是這樣講的,可以去看一下,設置如下:
1 new webpack.DefinePlugin({ 2 'process.env': { 3 NODE_ENV: options.dev ? JSON.stringify('development') : JSON.stringify('production') 4 } 5 })
5. 使用eslint修正代碼規范
通過eslint來檢查代碼的規范性,通過定義一套配置項,來規范代碼,這樣多人協作,寫出來的代碼也會比較優雅,不好的地方是,就是配置項太多,有些默認項設置我們不需要,但是確是處處限制我們,需要通過配置屏蔽掉,可以通過.eslintrc 文件或是package.json的eslintConfig,還有其他方式,可以到中文網看,這里我用的是package.json方式,如下:
1 ... 2 "eslintConfig": { 3 "parser": "babel-eslint", 4 "extends": "enough", 5 "env": { 6 "browser": true, 7 "node": true, 8 "commonjs": true, 9 "es6": true 10 }, 11 "rules": { 12 "linebreak-style": 0, 13 "indent": [2, 4], 14 "no-unused-vars": 0, 15 "no-console": 0 16 }, 17 "plugins": [ 18 "html" 19 ] 20 }, 21 ...
我們還需要安裝 npm install eslint eslint-config-enough eslint-loader --save-dev,eslint-config-enough是所謂的配置文件,這樣package.json的內容才能起效,但是不當當是這樣,對應編輯器也需要安裝對應的插件,sublime text 3需要安裝SublimeLinter、SublimeLinter-contrib-eslint插件。對於所有規則的詳解,可以去看官網,也可以去這里看,很詳細!
由於我們使用的是vue-loader,自然我們是希望能對.vue文件eslint,那么需要安裝eslint-plugin-html,在package.json中進行配置。然后對應webpack配置:
1 { 2 enforce: 'pre', 3 test: /.vue$/, 4 loader: 'eslint-loader', 5 exclude: /node_modules/ 6 }
我們會發現webpack v1和v2之間會有一些不同,比如webpack1對於預先加載器處理的執行是這樣的,
1 module: { 2 preLoaders: [ 3 { 4 test: /\.js$/, 5 loader: "eslint-loader" 6 } 7 ] 8 }
更多的不同可以到中文網看,很詳細,不做拓展。
6. alias vue指向問題
1 ... 2 alias: { 3 vue: 'vue/dist/vue' 4 }, 5 ...
Vue 最早會打包生成三個文件,一個是 runtime only 的文件 vue.common.js,一個是 compiler only 的文件 compiler.js,一個是 runtime + compiler 的文件 vue.js。vue.js = vue.common.js + compiler.js,默認package.json的main是指向vue.common.js,而template 屬性的使用一定要用compiler.js,因此需要在alias改變vue指向
7. devServer的使用
之前的項目中使用的是用express啟動http服務,webpack-dev-middleware+webpack-hot-middleware,這里會用到compiler+compilation,這個是webpack的編譯器和編譯過程的一些知識,也不是很懂,后續要去做做功課,應該可以加深對webpack運行機制的理解。這樣做的話,感覺復雜很多,對於webpack2.0 devServer似乎功能更強大更加完善了,所以直接使用就可以了。如下:
1 devServer: { 2 port: 8080, //端口號 3 proxy: { //方向代理 /api/auth/ => http://api.example.dev 4 '/api/auth/': { 5 target: 'http://api.example.dev', 6 changeOrigin: true, 7 pathRewrite: { '^/api': '' } 8 } 9 }, 10 publicPath: config.publicPath, 11 stats: { colors: true } 12 } 13 //changeOrigin會修改HTTP請求頭中的Host為target的域名, 這里會被改為api.example.dev 14 //pathRewrite用來改寫URL, 這里我們把/api前綴去掉,直接使用/auth/請求
webpack 2 打包實戰講解得非常好,非常棒。可以去看一下,一定會有所收獲!
8. 熱重載原理
webpack中文網,講的還算清楚,不過可能太笨,看起來還是雲里霧里的,似懂非懂的,補補課,好好看看。
9. localtunnel的使用
Localtunnel 是一個可以讓內網服務器暴露到公網上的開源項目,使用可以看這里,
1 $ npm install -g localtunnel 2 $ lt --port 8080 3 your url is: https://uhhzexcifv.localtunnel.me
這樣的話,可以把我們的本地網站暫時性地暴露到公網,可以對網站做一些線上線下對比,詳細內容可以去了解一下localtunnel,這里講的是通過上面配置,訪問https://uhhzexcifv.localtunnel.me,沒有達到理想效果,出現了Invalid Host header的錯誤,因為devServer缺少一個配置disableHostCheck: true,這樣的一個配置,很多文檔上面都沒有說明,字面上面的意思不要去檢查Host,這樣設置,便可以繞過這一層檢驗,設置的配置項在optionsSchema.json中,issue可以看這里
10. 升級webpack3.0
webpack3.0完美向下兼容,添加了些新特性,如范圍提升,魔法注釋 ”Magic Comments(暫時不知道怎么用),升級過程遇到Uncaught TypeError: Cannot read property 'call' of undefined的錯誤,最后在HtmlWebpackPlugin插件配置了chunksSortMode: 'dependency'解決了。
文章內容可能會更新,可以關注github
