Vue 3, TypeScript and webpack
Feb 15, 2022 typescript vue webpack
Installing Vue.js 3, TypeScript and webpack from scratch
One of my hobby projects has me wanting to use Vue.js. I started off using a CDN version of Vue along with a dynamic loader for loading individual .vue
files, but as the project was growing, I was finding it was becoming cumbersome with long load times as individual files were pulled down, plus I also wanted to also introduce TypeScript, so it was time to bring in a compile/bundle step for this part of the project.
There are various options for creating Vue.js applications, from using a CLI tool provided by the Vue project, up to and including just using a Visual Studio template to do so, but I didn't find any good information on what's actually going on under the hood when you use these options. I like to know what's going on in my build systems, and as I wanted to introduce it to an existing project, rather than start one from scratch (with the potential to possibly need to do this mutiple times), I set off to install the individual components one at a time. It's also my first time using npm in a while, so I've documented what I did to get up and running.
Prerequisite - Node.js
When I first started looking at this, as above I was using a Visual Studio template, which builds in support for https certificates for local dev and the like too. I haven't got that far in what's described below yet, but one issue I hit with the above was that I'd used the latest version of Node.js, which at the time of writing was 17.x, and it didn't seem to play well with this stack. As such, I downgraded to Node.js 16.x, which is the current LTS version. Whether or not the https issues will be addressed in 17, I don't know - I haven't looked into it as yet. Bottom line is that all of the below is written with Node.js 16 in mind.
webpack
The first thing I wanted to get up and running was the core bundler. There are several. I picked webpack. Before we install it, we need to create our initial package.json
. You can either use npm init
and run through the prompts to create it, or you can use a fairly minimal one:
{
"name": "vuetest",
"private": true
}
"private": true
prevents accidentally publishing something to npm you didn't mean to.
Once that's in place, you can then install webpack and the cli:
npm install --save-dev webpack webpack-cli
Now's also a good time to create a .gitignore file for the downloaded node modules, plus a dist/
folder which is where we'll be building the output to.
node_modules/
dist/
We also create a minimal webpack.config.js
, which controls the options for webpack when it runs.
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist'),
},
mode: 'development',
};
You can now run webpack from the command line, or we can add a small script to package.json
:
"scripts": {
"build": "webpack --config webpack.config.js"
}
We can now run a build using npm run build
, but before we do so, let's create some test files:
/src/test.js:
export function logtest(msg) {
console.log("Hello from test: " + msg);
}
/src/index.js:
import { logtest } from './test'
logtest("Hello World");
If you now run npm run build
, a main.js
file should be created in /dist
. In order to make use of it, create a minimal index.html
inside /dist
and open it in your browser:
<html>
<body>
<script src="main.js"></script>
</body>
</html>
All being well, you should see Hello from test: Hello World
being logged in the browser console.
We don't want to have the html file sat in the /dist
folder though, we want to also generate html output. Move index.html
to /src
, and remove the script tag - webpack will automatically insert a script tag when building. To build the HTML as well, we use the html-webpack-plugin:
npm install --save-dev html-webpack-plugin
We also need to add it to the webpack config in webpack.config.js
. First, require
it into the file:
const HtmlWebpackPlugin = require('html-webpack-plugin');
Then add a new plugins
section telling it to make use of our html file.
plugins: [
new HtmlWebpackPlugin({
template: 'src/index.html'
})
]
npm run build
now also builds the html file - it should create it inside /dist
, and manually opening it in the browser should work as before.
Rather than manually opening files though, it's better to serve them - webpack has a development server for this purpose:
npm install --save-dev webpack-dev-server
By default it serves from /public
, let's update the config to use our /dist
folder - again it's an extra section in webpack.config.js
:
devServer: {
static: {
directory: path.join(__dirname, 'dist'),
}
}
You can now start the webserver with npx webpack serve
and open a browser to localhost:8080
. Note that running this command also does a build, and also live reloads the page when things change, so we don't necessarily need to use the previous npm run build
anymore. We can augment that script with another one in package.json
:
"serve": "webpack serve --config webpack.config.js"
Now npm run serve
also works to build and load it. Note that neither of the scripts we've specified technically need the --config webpack.config.js
option, it would use that file by default - but that's another small bit of "magic" worth being aware of.
TypeScript
We've got our small webpack JavaScript and HTML bundler up and running, we can now add JavaScript modules and import them etc, and have live reloading going on - let's add TypeScript support for a better dev experience.
npm install --save-dev typescript ts-loader
As well as installing the typescript
package, we also install ts-loader
which is an addon for webpack dealing with TypeScript files. Now they're installed, we again need to update the webpack.config.js
to make use of them with some new sections:
resolve: {
extensions: ['.ts', '.tsx', '.js']
},
module:{
rules:[{
test: /\.tsx?$/,
loader: 'ts-loader'
}]
}
TypeScript
also needs a config file, called tsconfig.json
- for now we can just use a(n almost) blank one:
{}
We can now switch from using JavaScript files to TypeScript - update index.js
and test.js
to be .ts
files, and tweak the content:
test.ts
:
export function logtest(msg:string):void {
console.log("Hello from test: " + msg);
}
index.ts
:
import { logtest } from './test'
logtest("Hello World");
Also update the webpack.config.js
entry point to be /src/index.ts
instead of /src/index.js
. Building and running should result in the same behaviour as before. We can also "prove" that TypeScript is doing it's thing by adding an additional line of logging to index.ts
:
logtest(123); //causes build error: TS2345: Argument of type 'number' is not assignable to parameter of type 'string'.
Debugging - sourcemaps
One thing I know I'll probably spend a long time doing is debugging in the browser. Let's make that experience better.
Add debugger;
before the call to logtest();
in index.ts
. When the browser breaks at that point, the shown code isn't great:
exports.__esModule = true;
var test_1 = __webpack_require__(/*! ./test */ "./src/test.ts");
debugger;
(0, test_1.logtest)("Hello World");
It vaguely resembles the original source, but the source isn't exactly complex at the moment. As the project grows, the more this will stop looking like the original source. This is where sourcemaps come in - let's add support for them.
Firstly, since we're using TypeScript, we need to tell it to generate them in the tsconfig.json
:
{
"compilerOptions": {
"sourceMap": true
}
}
And we need to tell webpack about them too in the webpack.config.js
:
devtool: "source-map"
(There are various options available here, "source-map"
is a simple starting point.)
If you build and run it now, the browser dev tools show what you'd expect:
import { logtest } from './test'
debugger;
logtest("Hello World");
And stepping in to the logtest
function works as expected:
export function logtest(msg:string ):void {
console.log("Hello from test: " + msg);
}
Vue.js
We've now got our static site building and bundling TypeScript, with good debugging support. Time to add Vue.js. At the time of writing, Vue.js 3 is now the current version, so you don't need to use any of the @next
type syntax to install it:
npm install --save vue
npm install --save-dev vue-loader
Once installed, as before, we need to tell webpack about it in webpack.config.js
. Firstly include it:
const { VueLoaderPlugin } = require('vue-loader');
Then instantiate the plugin alongside the HtmlWebpackPlugin
:
new VueLoaderPlugin()
The .vue
files we'll be using require runtime compilation/transformation - at the time of writing I haven't yet worked out precompilation of them, so we need to alias vue to its runtime bundler. To do so, add an alias
section within the existing resolve
section - alongside where we're telling it which extensions to use:
resolve: {
extensions: ['.ts', '.tsx', '.js'],
alias: {
'vue':'vue/dist/vue.esm-bundler.js'
}
}
We also need to update our rules files, firstly to tell webpack to use the vue-loader
for .vue
files, and secondly to tell the ts-loader
to also work with .vue
files:
rules: [{
test: /\.tsx?$/,
loader: 'ts-loader',
options: {
appendTsSuffixTo: [/\.vue$/]
}
}, {
test: /\.vue$/,
loader: 'vue-loader'
}]
Finally we need to create a small shims file to get Vue to work nicely with our TypeScript setup - create vue-shims.d.ts
:
declare module "*.vue" {
import type { DefineComponent } from "vue";
const component: DefineComponent<{}, {}, any>;
export default component;
}
We can now update our application to make use of Vue. Firstly, let's introduce an element to mount it in in /src/index.html
:
We don't need test.ts
anymore, let's delete it and create a test vue instead, in /src/test.vue
:
<template>
<div>{{Name}}</div>
</template>
<script lang="ts">
class Test {
Name:string = "";
}
export default {
data(): Test {
return {
Name: "abcd"
}
}
}
</script>
And now update /src/index.ts
to be the entry point for the new Vue application:
import { createApp } from 'vue'
import VueTest from './test.vue'
createApp({
components: {
"vue-test": VueTest
},
template: "<vue-test />"
}).mount("#app");
You should now be able to build and load the site as before, but with it running the simple Vue app we've created. Once again, to prove that TypeScript is working as expected within the .vue
files, we can update the test.vue
file to return a number
where a string
should be being returned:
export default {
data(): Test {
return {
Name: 123
}
}
}
Doing so should cause a build issue:
TS2322: Type 'number' is not assignable to type 'string'.
Next Steps
That's it for now. Having done it, it all seems pretty straightforward, but coming from a .NET viewpoint it took me a while to gather the relevant information and get things going. This is a good start for developing Vue apps, my next steps are to integrate it into my Visual Studio pipeline to get it to run alongside the .NET projects that will form the backend.
Back to posts