標籤:

Fis3構建遷移Webpack之路

Webpack從2015年9月第一個版本橫空初始至今已逾2載。它的出現,顛覆了一大批主流構建如Ant、Grunt和Gulp等等。騰訊NOW直播IVWEB團隊之前一直採用Fis構建,本篇文章主要介紹從Fis遷移到webpack遇到的問題和背後的黑科技,內容包括inline-resource、多頁面構建、資源壓縮、文件hash、文件目錄規則等等。

為什麼要遷移至webpack?

有兩個層面的原因:

  • 首先webpack的社區生態火爆,插件齊全並且維護更新的很頻繁,遇到了問題,比較容易解決。
  • webpack裡面有happypack多實例構建方案、code spliting按需載入文件等方案, 可以有效的進行打包構建持續優化, 這些在Fis裡面是缺少的。

區分構建的開發or生產環境?

"scripts": { "dev": "cross-env NODE_ENV=dev nodemon --watch webpack.config.js --exec "webpack-dev-server --config webpack.config.js --env development" --progress --colors", "build": "webpack --config webpack.config.js --env production --progress --colors", ... },

通過在package.json中注入環境變數的方式,注入NODE_ENV=dev代表開發環境,默認為生產環境。這裡使用cross-env的原因是:windows下 在package.json中直接使用 NODE_ENV=dev 不生效,需寫成 set NODE_ENV=dev,cross-env的寫法兼容各個操作系統。

資源內聯 (inline-resource)

inline-resource的好處是可以減少css,js等的請求數,同時html載入的時候即可同時載入了這些內聯的css、js等靜態資源,可以有效的減少白屏或者頁面閃動的問題。

這裡的內聯分為2種,一種是靜態的html片段,css,js等,這些資源一開始就存在項目的某個目錄下;另一種是構建過程中動態生成的css,js文件。

對於html的內聯,寫法如下:

${require(raw-loader!../src/assets/inline/meta.html)}

對於js的內聯,需要增加babel-loader將ES6的語法進行轉換,避免瀏覽器直接解析導致報錯。寫法如下:

<script>${require(raw-loader!babel-loader!../src/node_modules/@tencent/report-whitelist/lib/index.js)}</script>

說明:不能將html-loader和html-webpack-plugin同時使用,html-loader會導致默認的ejs模板引擎語法解析實效,造成 ${} 和 <% = %>等語法不生效

上面講述了如何內聯靜態的資源文件,那麼如何內聯構建過程中動態生成的資源文件呢?這裡需要藉助html-webpack-inline-source-plugin來增強html-webpack-plugin的功能。比如:將構建過程中生成的css文件inline到html模板裡面去。

const HtmlWebpackPlugin = require(html-webpack-plugin);const HtmlWebpackInlineSourcePlugin = require(html-webpack-inline-source-plugin); new HtmlWebpackPlugin({ inlineSource: isDev ? undefined : \.css$, template: __dirname + /template/index.tmpl.html, filename: activity.html, inject: true, }), new HtmlWebpackInlineSourcePlugin(), ...

上面這段代碼,html-webpack-plugin本身並不具備inlineSource的屬性。引入了html-webpack-inline-source-plugin之後,就可以通過inlineSource屬性來匹配哪些文件需要動態的內聯進html模板文件中了。

多頁面構建

多頁面構建,或者稱為通配(wildcards)構建。即需要構建的頁面數量是不確定的,可能A業務有3張頁面,B業務有5張頁面。因此,我們不能把entry寫死了:

entry: { activity: ./src/pages/activity/init.js, // 深海尋寶活動首頁 my-reward: ./src/pages/my-reward/init.js, // 我的獎勵 exchange: ./src/pages/exchange/init.js // 線下兌換獎品},

為什麼上面的寫法不可取呢?很明顯:上面的寫法把entry寫死了,這並不通用。後面如果產品需求發生改變,需要新增一張頁面,就需要手動修改構建腳本。我們需要的entry是:./src/pages/**/init.js,它能夠像一些linux的命令,具備匹配某個規則的所有結果的能力。這裡的思路是藉助**glob**,達到動態entry的目的。

entry: glob.sync(./src/pages/**/init.js),

在webpack構建中,一個頁面需要一個與之對應的HtmlWebpackPlugin實例,N個頁面需要N個與之對應的HtmlWebpackPlugin。此處需要動態的設置HtmlWebpackPlugin的實例個數。

const newEntry = {};Object.keys(config.entry).map((index) => { const entry = config.entry[index]; const match = entry.match(//pages/(.*)/init.js/); const pageName = match && match[1]; newEntry[pageName] = entry; config.plugins.push( new HtmlWebpackPlugin({ inlineSource: isDev ? undefined: \.css$, template: __dirname + /template/index.tmpl.html, filename: `${pageName}.html`, chunks: [pageName], inject: true }) );});config.entry = newEntry;

html、css和js壓縮

對於html文件裡面的內容壓縮可以通過給html-webpack-plugin設置minify參數,html-webpack-plugin的壓縮配置其實是直接集成了 html-minifier,因此minify的參數設置可以直接參考html-minifier的文檔。

config.plugins.push( new HtmlWebpackPlugin({ inlineSource: isDev ? undefined: \.css$, template: __dirname + /template/index.tmpl.html, filename: `${pageName}.html`, chunks: [pageName], inject: true, minify: { minifyJS: true, // 僅壓縮內聯在html裡面的js minifyCSS: true, // 僅壓縮內聯在html裡面的css html5: true, // 以html5的文檔格式解析html的模板文件 removeComments: false, // 不刪除Html文件裡面的注釋 collapseWhitespace: true, // 刪除空格 preserveLineBreaks: false // 刪除換行 } }));

設置了上面的minify參數後,看到生成的html文件的內容全部在1行上,需要注意的是:minifyJS和minifyCSS只會壓縮內聯在這個html文件的css和js內容,對於單獨的css文件和js文件並不會壓縮。 那麼打包出來的css和js文件如何壓縮呢?

對於css文件壓縮,直接開啟css-loader的壓縮參數參數minimize為true即可:

{ test: /.scss$/, use: ExtractTextPlugin.extract({ fallback: style-loader, use: [ { loader: "css-loader", options: { // 設置css-loader的minimize參數為true minimize: true } }, { loader: "sass-loader" } ] })},

css-loader開啟壓縮可能會報錯 Module build failed: BrowserlistError: unkonwn version 61 and _chr,解決辦法:

$ npm i caniuse-db —save #更新caniuse-db到最新版本

對於js文件的壓縮,可以通過引入 webpack 內置的 UglifyJsPlugin:

const webpack = require(webpack);plugins: [ ... new webpack.optimize.UglifyJsPlugin(), ...],

文件Hash

每次功能發布上線,都需要重新構建一次源代碼,生成一個新的文件版本列表。此處文件Hash的方式就是一種版本管理的方式,發布時替換有變化的版本的文件,達到增量更新的效果。此處Hash策略是:根據文件內容進行hash,取8位。

JS文件:

output: { filename: [name]_[chunkhash:8].js, // 進行js腳本hash path: path.resolve(__dirname, public/), publicPath: isDev ? / : cdnUrl + /,},

Css文件:

plugins: [ new CleanWebpackPlugin([./public]), new ExtractTextPlugin([name]_[contenthash:8].css), // css文件hash new webpack.optimize.UglifyJsPlugin(), ...]

Img文件:

rules: [ { test: /.(png|svg|jpg|gif)$/, use: { loader: file-loader, options: { name: [name]_[hash:8].[ext], // img文件hash } } }, ...]

多終端適配

開發過程中,不同解析度的瀏覽器適配是個讓前端開發者頭疼的問題。手淘的rem方案完美解決了這個問題,它的核心思想是頁面載入時動態設置body的font-size值和rem和px轉換的單位。

為了不改變編程習慣,開發過程中仍然使用px單位來作為基礎布局長度單位,以750px寬度的視覺稿作為基準,設置rem和px的轉換單位為1rem=75px。那麼px2rem如何集成進webpack中呢?首先增加css的解析[px2rem-loader](px2rem-loader),然後在html頭部引入[lib-flexible](amfe/lib-flexible)文件。

{ test: /.scss$/, use: ExtractTextPlugin.extract({ fallback: style-loader, use: [ { loader: "css-loader" }, { loader: "px2rem-loader", // 增加px2rem-loader,並且設置rem單位為75px options: { remUnit: 75 } }, { loader: "sass-loader" } ] })},

其它feature

  • 開發環境支持WDS: webpack3.x版本自帶webpack-dev-server,開發環境中開啟WDS。這樣依賴的文件發生變化後,會自動增量構建並且刷新瀏覽器
  • 支持HMR: webpack.config.js文件內容變化後,會觸發熱更新邏輯,此處通過nodemon來守護webpack的構建進程,eg:

"scripts": { "dev": "cross-env NODE_ENV=dev nodemon --watch webpack.config.js --exec "webpack-dev-server --config webpack.config.js --env development" --progress --colors" ... },

由於篇幅原因,關於webpack的打包優化將會用另外一篇文章介紹,敬請期待~

參考文檔

  • webpack 官方文檔
  • 一本介紹webpack比較全面的教程
  • html-webpack-plugin文檔
  • Wildcards in entry points · Issue #370
  • BrowserslistError: Unknown version 55 of and_chr · Issue #340

打個廣告

騰訊NOW直播工程化解決方案feflow正式開源啦~ 它集項目初始化、開發構建、代碼規範、代碼發布 於一身的工具。


推薦閱讀:

vue.js不用yeoman怎麼搭建工程化結構?另外常用的vue的包又有哪些
Webpack 的核心開發者 Sean Larkin 入駐 SegmentFault 了
requirejs/seajs和webpack/browserify各自的優勢以及如何取捨?
webpack 之 代碼拆分

TAG:webpack |